From 1f7004696117aef9af9717ab9bbe2d1f857bcef9 Mon Sep 17 00:00:00 2001 From: 1 Date: Sat, 31 Aug 2024 17:18:18 +0300 Subject: [PATCH 01/41] Readme upd to ease troubleshooting and fixing --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3258335..15fe7b8 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ LinkedIn_AIHawk steps in as a game-changing solution to these challenges. It's n ## Installation **Please watch this video to set up your LinkedIn_AIHawk: [How to set up LinkedIn_AIHawk](https://youtu.be/gdW9wogHEUM) - https://youtu.be/gdW9wogHEUM** - +0. **Confirmed succesfull runs OSs & Python**: Python 3.10, 3.11.9(64b), 3.12.5(64b) . Windows 10, Ubuntu 22 1. **Download and Install Python:** Ensure you have the last Python version installed. If not, download and install it from Python's official website. For detailed instructions, refer to the tutorials: @@ -507,11 +507,16 @@ TODO ): ## Troubleshooting +- **Carefully read logs and output :** Most of the errors are verbosely reflected just watch the output and try to find the root couse. +- **If nothing works by unknown reason:** Use tested OS. Reboot and/or update OS. Use new clean venv. Try update Python to the tested version. - **ChromeDriver Issues:** Ensure ChromeDriver is compatible with your installed Chrome version. - **Missing Files:** Verify that all necessary files are present in the data folder. -- **Invalid YAML:** Check your YAML files for syntax errors. - - If you encounter any issues, you can open an issue on [GitHub](https://github.com/feder-cr/linkedIn_auto_jobs_applier_with_AI/issues). I'll be more than happy to assist you! +- **Invalid YAML:** Check your YAML files for syntax errors . Try to use external YAML validators e.g. https://www.yamllint.com/ +- **OpenAI endpoint isues**: Try to check possible limits\blocking at their side + +If you encounter any issues, you can open an issue on [GitHub](https://github.com/feder-cr/linkedIn_auto_jobs_applier_with_AI/issues). + Please add valuable details to the subject and to the description. If you need new feature then please reflect this. + I'll be more than happy to assist you! ## Conclusion From d964f599bed339bee2404898516fd20ba581df6c Mon Sep 17 00:00:00 2001 From: user Date: Sat, 31 Aug 2024 20:19:33 +0200 Subject: [PATCH 02/41] Added support for Ollama running locally or publicly hosted api --- README.md | 8 +++++++- data_folder/secrets.yaml | 3 ++- data_folder_example/secrets.yaml | 3 ++- main.py | 12 ++++++------ src/gpt.py | 25 +++++++++++++++++-------- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 15fe7b8..de60c0e 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,13 @@ This file contains sensitive information. Never share or commit this file to ver - Replace with your OpenAI API key for GPT integration - To obtain an API key, follow the tutorial at: https://medium.com/@lorenzozar/how-to-get-your-own-openai-api-key-f4d44e60c327 - Note: You need to add credit to your OpenAI account to use the API. You can add credit by visiting the [OpenAI billing dashboard](https://platform.openai.com/account/billing). - +- `openai_api_free_hosted_url`: + - Optional paramter, if you want to use freely hosted GPT model, set `openai_api_key: "freehosted"` and `openai_api_free_hosted_url` with the URL of the endpoint +- Ollama local support + - If you want to use Ollama which is deployed locally, leave `openai_api_key` blank. + - To setup Ollama to run locally follow the instructions here: [Ollama installation](https://github.com/ollama/ollama). + - Download mistral model by pulling mistral:v0.3 + ### 2. config.yaml diff --git a/data_folder/secrets.yaml b/data_folder/secrets.yaml index ad24cd8..9d0bfb3 100644 --- a/data_folder/secrets.yaml +++ b/data_folder/secrets.yaml @@ -1,3 +1,4 @@ email: myemaillinkedin@gmail.com password: ImpossiblePassowrd10 -openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR \ No newline at end of file +openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR +openai_api_free_hosted_url: https://api.pawan.krd/cosmosrp/v1 \ No newline at end of file diff --git a/data_folder_example/secrets.yaml b/data_folder_example/secrets.yaml index ad24cd8..9d0bfb3 100644 --- a/data_folder_example/secrets.yaml +++ b/data_folder_example/secrets.yaml @@ -1,3 +1,4 @@ email: myemaillinkedin@gmail.com password: ImpossiblePassowrd10 -openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR \ No newline at end of file +openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR +openai_api_free_hosted_url: https://api.pawan.krd/cosmosrp/v1 \ No newline at end of file diff --git a/main.py b/main.py index 9685677..82ed33f 100644 --- a/main.py +++ b/main.py @@ -101,7 +101,7 @@ class ConfigValidator: @staticmethod def validate_secrets(secrets_yaml_path: Path) -> tuple: secrets = ConfigValidator.validate_yaml_file(secrets_yaml_path) - mandatory_secrets = ['email', 'password', 'openai_api_key'] + mandatory_secrets = ['email', 'password'] for secret in mandatory_secrets: if secret not in secrets: @@ -114,7 +114,7 @@ class ConfigValidator: if not secrets['openai_api_key']: raise ConfigError(f"OpenAI API key cannot be empty in secrets file {secrets_yaml_path}.") - return secrets['email'], str(secrets['password']), secrets['openai_api_key'] + return secrets['email'], str(secrets['password']), secrets['openai_api_key'], secrets['openai_api_free_hosted_url'] class FileManager: @staticmethod @@ -158,7 +158,7 @@ def init_browser() -> webdriver.Chrome: except Exception as e: raise RuntimeError(f"Failed to initialize browser: {str(e)}") -def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_key: str): +def create_and_run_bot(email, password, parameters, openai_api_key, openai_api_free_hosted_url): try: style_manager = StyleManager() resume_generator = ResumeGenerator() @@ -175,7 +175,7 @@ def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_k browser = init_browser() login_component = LinkedInAuthenticator(browser) apply_component = LinkedInJobManager(browser) - gpt_answerer_component = GPTAnswerer(openai_api_key) + gpt_answerer_component = GPTAnswerer(openai_api_key, openai_api_free_hosted_url) bot = LinkedInBotFacade(login_component, apply_component) bot.set_secrets(email, password) bot.set_job_application_profile_and_resume(job_application_profile_object, resume_object) @@ -197,12 +197,12 @@ def main(resume: Path = None): 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) + email, password, openai_api_key, openai_api_free_hosted_url = 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) + create_and_run_bot(email, password, parameters, openai_api_key, openai_api_free_hosted_url) except ConfigError as ce: print(f"Configuration error: {str(ce)}") print("Refer to the configuration guide for troubleshooting: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration") diff --git a/src/gpt.py b/src/gpt.py index 371c0c2..684a4de 100644 --- a/src/gpt.py +++ b/src/gpt.py @@ -3,7 +3,7 @@ import os import re import textwrap from datetime import datetime -from typing import Dict, List +from typing import Dict, List, Union from pathlib import Path from dotenv import load_dotenv from langchain_core.messages.ai import AIMessage @@ -11,6 +11,7 @@ from langchain_core.output_parsers import StrOutputParser from langchain_core.prompt_values import StringPromptValue from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI +from langchain_ollama import ChatOllama from Levenshtein import distance import src.strings as strings @@ -20,7 +21,7 @@ load_dotenv() class LLMLogger: - def __init__(self, llm: ChatOpenAI): + def __init__(self, llm: Union[ChatOpenAI, ChatOllama]): self.llm = llm @staticmethod @@ -78,12 +79,12 @@ class LLMLogger: class LoggerChatModel: - def __init__(self, llm: ChatOpenAI): + def __init__(self, llm: Union[ChatOpenAI, ChatOllama]): self.llm = llm def __call__(self, messages: List[Dict[str, str]]) -> str: # Call the LLM with the provided messages and log the response. - reply = self.llm(messages) + reply = self.llm.invoke(messages) parsed_reply = self.parse_llmresult(reply) LLMLogger.log_request(prompts=messages, parsed_reply=parsed_reply) return reply @@ -113,10 +114,18 @@ 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.4) - ) + def __init__(self, openai_api_key, openai_api_free_hosted_url): + if openai_api_key == "": + print('Using locally hosted mistral:v0.3') + self.llm_model = ChatOllama(model = "mistral:v0.3", temperature = 0.4, num_predict = 256) + elif openai_api_key == "freehosted": + print('Using free hosted gpt-4o-mini') + self.llm_model = ChatOpenAI(model_name="gpt-4o-mini", openai_api_key="anything", temperature=0.4, + base_url=openai_api_free_hosted_url) + else: + print("Using gpt-4o-mini") + self.llm_model = ChatOpenAI(model_name="gpt-4o-mini", openai_api_key=openai_api_key, temperature=0.4) + self.llm_cheap = LoggerChatModel(self.llm_model) @property def job_description(self): return self.job.description From ca4f56833a2834d068dc8cf0cc62ebd9f80d0873 Mon Sep 17 00:00:00 2001 From: user Date: Sat, 31 Aug 2024 22:58:40 +0200 Subject: [PATCH 03/41] Added support for ollama endpoint and defining the LLM model in the config --- README.md | 17 +++++++++-------- data_folder/config.yaml | 6 +++++- data_folder/secrets.yaml | 3 +-- data_folder_example/config.yaml | 4 ++++ data_folder_example/secrets.yaml | 3 +-- main.py | 15 ++++++--------- src/gpt.py | 23 ++++++++++++----------- 7 files changed, 38 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index de60c0e..b396b3f 100644 --- a/README.md +++ b/README.md @@ -148,17 +148,10 @@ This file contains sensitive information. Never share or commit this file to ver - Replace with your LinkedIn account email address - `password: [Your LinkedIn password]` - Replace with your LinkedIn account password -- `openai_api_key: [Your OpenAI API key]` +- `llm_api_key: [Your OpenAI or Ollama API key]` - Replace with your OpenAI API key for GPT integration - To obtain an API key, follow the tutorial at: https://medium.com/@lorenzozar/how-to-get-your-own-openai-api-key-f4d44e60c327 - Note: You need to add credit to your OpenAI account to use the API. You can add credit by visiting the [OpenAI billing dashboard](https://platform.openai.com/account/billing). -- `openai_api_free_hosted_url`: - - Optional paramter, if you want to use freely hosted GPT model, set `openai_api_key: "freehosted"` and `openai_api_free_hosted_url` with the URL of the endpoint -- Ollama local support - - If you want to use Ollama which is deployed locally, leave `openai_api_key` blank. - - To setup Ollama to run locally follow the instructions here: [Ollama installation](https://github.com/ollama/ollama). - - Download mistral model by pulling mistral:v0.3 - ### 2. config.yaml @@ -217,6 +210,14 @@ This file defines your job search parameters and bot behavior. Each section cont - Sales - Marketing ``` +- `llm_model_type`: + - Choose the model type, supported: openai / ollama +- `llm_model`: + - Choose the LLM model, currently supported: + - openai: gpt-4o + - ollama: llama2, mistral:v0.3 +- `llm_api_url`: + - Link of the API endpoint for the LLM model ### 3. plain_text_resume.yaml diff --git a/data_folder/config.yaml b/data_folder/config.yaml index 58a6f1c..e28e58a 100644 --- a/data_folder/config.yaml +++ b/data_folder/config.yaml @@ -39,4 +39,8 @@ companyBlacklist: titleBlacklist: - word1 - - word2 \ No newline at end of file + - word2 + +llm_model_type: [openai / ollama] +llm_model: ['gpt-4o' / 'mistral:v0.3'] +llm_api_url: [https://api.pawan.krd/cosmosrp/v1', http://127.0.0.1:11434/] \ No newline at end of file diff --git a/data_folder/secrets.yaml b/data_folder/secrets.yaml index 9d0bfb3..c218803 100644 --- a/data_folder/secrets.yaml +++ b/data_folder/secrets.yaml @@ -1,4 +1,3 @@ email: myemaillinkedin@gmail.com password: ImpossiblePassowrd10 -openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR -openai_api_free_hosted_url: https://api.pawan.krd/cosmosrp/v1 \ No newline at end of file +llm_api_key: 'sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR' \ No newline at end of file diff --git a/data_folder_example/config.yaml b/data_folder_example/config.yaml index 6e362be..5f0a83f 100644 --- a/data_folder_example/config.yaml +++ b/data_folder_example/config.yaml @@ -37,3 +37,7 @@ companyBlacklist: - Crossover titleBlacklist: + +llm_model_type: openai +llm_model: 'gpt-4o' +llm_api_url: https://api.pawan.krd/cosmosrp/v1' \ No newline at end of file diff --git a/data_folder_example/secrets.yaml b/data_folder_example/secrets.yaml index 9d0bfb3..c218803 100644 --- a/data_folder_example/secrets.yaml +++ b/data_folder_example/secrets.yaml @@ -1,4 +1,3 @@ email: myemaillinkedin@gmail.com password: ImpossiblePassowrd10 -openai_api_key: sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR -openai_api_free_hosted_url: https://api.pawan.krd/cosmosrp/v1 \ No newline at end of file +llm_api_key: 'sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR' \ No newline at end of file diff --git a/main.py b/main.py index 82ed33f..af1f5b7 100644 --- a/main.py +++ b/main.py @@ -111,10 +111,7 @@ class ConfigValidator: raise ConfigError(f"Invalid email format in secrets file {secrets_yaml_path}.") if not secrets['password']: raise ConfigError(f"Password cannot be empty in secrets file {secrets_yaml_path}.") - if not secrets['openai_api_key']: - raise ConfigError(f"OpenAI API key cannot be empty in secrets file {secrets_yaml_path}.") - - return secrets['email'], str(secrets['password']), secrets['openai_api_key'], secrets['openai_api_free_hosted_url'] + return secrets['email'], str(secrets['password']), secrets['llm_api_key'] class FileManager: @staticmethod @@ -158,14 +155,14 @@ def init_browser() -> webdriver.Chrome: except Exception as e: raise RuntimeError(f"Failed to initialize browser: {str(e)}") -def create_and_run_bot(email, password, parameters, openai_api_key, openai_api_free_hosted_url): +def create_and_run_bot(email, password, parameters, llm_api_key): try: style_manager = StyleManager() resume_generator = ResumeGenerator() with open(parameters['uploads']['plainTextResume'], "r") as file: plain_text_resume = file.read() resume_object = Resume(plain_text_resume) - resume_generator_manager = FacadeManager(openai_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output")) + resume_generator_manager = FacadeManager(llm_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() os.system('cls' if os.name == 'nt' else 'clear') @@ -175,7 +172,7 @@ def create_and_run_bot(email, password, parameters, openai_api_key, openai_api_f browser = init_browser() login_component = LinkedInAuthenticator(browser) apply_component = LinkedInJobManager(browser) - gpt_answerer_component = GPTAnswerer(openai_api_key, openai_api_free_hosted_url) + gpt_answerer_component = GPTAnswerer(parameters, llm_api_key) bot = LinkedInBotFacade(login_component, apply_component) bot.set_secrets(email, password) bot.set_job_application_profile_and_resume(job_application_profile_object, resume_object) @@ -197,12 +194,12 @@ def main(resume: Path = None): 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, openai_api_free_hosted_url = ConfigValidator.validate_secrets(secrets_file) + email, password, llm_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, openai_api_free_hosted_url) + create_and_run_bot(email, password, parameters, llm_api_key) except ConfigError as ce: print(f"Configuration error: {str(ce)}") print("Refer to the configuration guide for troubleshooting: https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/blob/main/readme.md#configuration") diff --git a/src/gpt.py b/src/gpt.py index 684a4de..bc71e8b 100644 --- a/src/gpt.py +++ b/src/gpt.py @@ -114,17 +114,18 @@ class LoggerChatModel: class GPTAnswerer: - def __init__(self, openai_api_key, openai_api_free_hosted_url): - if openai_api_key == "": - print('Using locally hosted mistral:v0.3') - self.llm_model = ChatOllama(model = "mistral:v0.3", temperature = 0.4, num_predict = 256) - elif openai_api_key == "freehosted": - print('Using free hosted gpt-4o-mini') - self.llm_model = ChatOpenAI(model_name="gpt-4o-mini", openai_api_key="anything", temperature=0.4, - base_url=openai_api_free_hosted_url) - else: - print("Using gpt-4o-mini") - self.llm_model = ChatOpenAI(model_name="gpt-4o-mini", openai_api_key=openai_api_key, temperature=0.4) + def __init__(self, config, llm_api_key): + llm_model_type = config['llm_model_type'] + llm_model = config['llm_model'] + llm_api_url = config['llm_api_url'] + + print('Using {0} with {1} from {2}'.format(llm_model_type, llm_model, llm_api_url)) + + if llm_model_type == "ollama": + self.llm_model = ChatOllama(model=llm_model, temperature = 0.4, base_url=llm_api_url) + elif llm_model_type == "openai": + self.llm_model = ChatOpenAI(model_name=llm_model, openai_api_key=llm_api_key, temperature=0.4, + base_url=llm_api_url) self.llm_cheap = LoggerChatModel(self.llm_model) @property def job_description(self): From be8ddb8241bc0ca960fa7fec7f5376f4a7818c9b Mon Sep 17 00:00:00 2001 From: Maurice McCabe Date: Sat, 31 Aug 2024 14:56:37 -0700 Subject: [PATCH 04/41] resume generator cmdline utility --- .gitignore | 2 + README.md | 21 +++++ assets/resume_liam_murphy.txt | 55 ++++++++++++ assets/resume_schema.yaml | 132 +++++++++++++++++++++++++++ requirements.txt | Bin 670 -> 710 bytes resume_yaml_generator.py | 164 ++++++++++++++++++++++++++++++++++ 6 files changed, 374 insertions(+) create mode 100644 assets/resume_liam_murphy.txt create mode 100644 assets/resume_schema.yaml create mode 100644 resume_yaml_generator.py diff --git a/.gitignore b/.gitignore index 4e73720..1e8e081 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ generated_cv* .vscode chrome_profile answers.json +resume.yaml +resume.yaml_validation_report.txt diff --git a/README.md b/README.md index 3258335..78c669e 100644 --- a/README.md +++ b/README.md @@ -452,6 +452,27 @@ Each section has specific fields to fill out: willing_to_undergo_drug_tests: "No" willing_to_undergo_background_checks: "Yes" ``` +### 4. Generating plain_text_resume.yaml from a Text Resume + +To simplify the process of creating your `plain_text_resume.yaml` file, you can use the provided script to generate it from a text-based resume. Follow these steps: + +1. Prepare your resume in a plain text format (.txt file). + +2. Place your text resume in the `data_folder` directory. + +3. Run the following command: + + ```bash + python generate_resume_yaml.py --input data_folder/your_resume.txt --output data_folder/plain_text_resume.yaml + ``` + + Replace `your_resume.txt` with the actual name of your text resume file. + +4. The script will generate a `plain_text_resume.yaml` file in the `data_folder` directory. + +5. Review the generated YAML file and make any necessary adjustments to ensure all information is correct and complete. + +This automated process helps in creating a structured YAML file from your existing resume, saving time and reducing the chance of errors in manual data entry. ### PLUS. data_folder_example diff --git a/assets/resume_liam_murphy.txt b/assets/resume_liam_murphy.txt new file mode 100644 index 0000000..30b5402 --- /dev/null +++ b/assets/resume_liam_murphy.txt @@ -0,0 +1,55 @@ +Liam Murphy +Galway, Ireland +Email: liam.murphy@gmail.com | LinkedIn: liam-murphy +GitHub: liam-murphy | Phone: +353 871234567 + +Education +Bachelor's Degree in Computer Science +National University of Ireland, Galway (GPA: 4/4) +Graduation Year: 2020 + +Experience +Co-Founder & Software Engineer +CryptoWave Solutions (03/2021 - Present) +Location: Ireland | Industry: Blockchain Technology + +Co-founded and led a startup specializing in app and software development with a focus on blockchain technology +Provided blockchain consultations for 10+ companies, enhancing their software capabilities with secure, decentralized solutions +Developed blockchain applications, integrated cutting-edge technology to meet client needs and drive industry innovation +Research Intern +National University of Ireland, Galway (11/2022 - 03/2023) +Location: Galway, Ireland | Industry: IoT Security Research + +Conducted in-depth research on IoT security, focusing on binary instrumentation and runtime monitoring +Performed in-depth study of the MQTT protocol and Falco +Developed multiple software components including MQTT packet analysis library, Falco adapter, and RML monitor in Prolog +Authored thesis "Binary Instrumentation for Runtime Monitoring of Internet of Things Systems Using Falco" +Software Engineer +University Hospital Galway (05/2022 - 11/2022) +Location: Galway, Ireland | Industry: Healthcare IT + +Integrated and enforced robust security protocols +Developed and maintained a critical software tool for password validation used by over 1,600 employees +Played an integral role in the hospital's cybersecurity team +Projects +JobBot +AI-driven tool to automate and personalize job applications on LinkedIn, gained over 3000 stars on GitHub, improving efficiency and reducing application time +Link: JobBot + +mqtt-packet-parser +Developed a Node.js module for parsing MQTT packets, improved parsing efficiency by 40% +Link: mqtt-packet-parser + +Achievements +Winner of an Irish public competition - Won first place in a public competition with a perfect score of 70/70, securing a Software Developer position at University Hospital Galway +Galway Merit Scholarship - Awarded annually from 2018 to 2020 in recognition of academic excellence and contribution +GitHub Recognition - Gained over 3000 stars on GitHub with JobBot project +Certifications +C1 + +Languages +English - Native +Spanish - Professional +Interests +Full-Stack Development, Software Architecture, IoT system design and development, Artificial Intelligence, Cloud Technologies + diff --git a/assets/resume_schema.yaml b/assets/resume_schema.yaml new file mode 100644 index 0000000..9a86f2f --- /dev/null +++ b/assets/resume_schema.yaml @@ -0,0 +1,132 @@ +# YAML Schema for plain_text_resume.yaml + +personal_information: + type: object + properties: + name: {type: string} + surname: {type: string} + date_of_birth: {type: string, format: date} + country: {type: string} + city: {type: string} + address: {type: string} + phone_prefix: {type: string, format: phone_prefix} + phone: {type: string, format: phone} + email: {type: string, format: email} + github: {type: string, format: uri} + linkedin: {type: string, format: uri} + required: [name, surname, date_of_birth, country, city, address, phone_prefix, phone, email] + +education_details: + type: array + items: + type: object + properties: + degree: {type: string} + university: {type: string} + gpa: {type: string} + graduation_year: {type: string} + field_of_study: {type: string} + exam: + type: object + additionalProperties: {type: string} + required: [degree, university, gpa, graduation_year, field_of_study] + +experience_details: + type: array + items: + type: object + properties: + position: {type: string} + company: {type: string} + employment_period: {type: string} + location: {type: string} + industry: {type: string} + key_responsibilities: + type: object + additionalProperties: {type: string} + skills_acquired: + type: array + items: {type: string} + required: [position, company, employment_period, location, industry, key_responsibilities, skills_acquired] + +projects: + type: array + items: + type: object + properties: + name: {type: string} + description: {type: string} + link: {type: string, format: uri} + required: [name, description] + +achievements: + type: array + items: + type: object + properties: + name: {type: string} + description: {type: string} + required: [name, description] + +certifications: + type: array + items: {type: string} + +languages: + type: array + items: + type: object + properties: + language: {type: string} + proficiency: {type: string, enum: [Native, Fluent, Intermediate, Beginner]} + required: [language, proficiency] + +interests: + type: array + items: {type: string} + +availability: + type: object + properties: + notice_period: {type: string} + required: [notice_period] + +salary_expectations: + type: object + properties: + salary_range_usd: {type: string} + required: [salary_range_usd] + +self_identification: + type: object + properties: + gender: {type: string} + pronouns: {type: string} + veteran: {type: string, enum: [Yes, No]} + disability: {type: string, enum: [Yes, No]} + ethnicity: {type: string} + required: [gender, pronouns, veteran, disability, ethnicity] + +legal_authorization: + type: object + properties: + eu_work_authorization: {type: string, enum: [Yes, No]} + us_work_authorization: {type: string, enum: [Yes, No]} + requires_us_visa: {type: string, enum: [Yes, No]} + requires_us_sponsorship: {type: string, enum: [Yes, No]} + requires_eu_visa: {type: string, enum: [Yes, No]} + legally_allowed_to_work_in_eu: {type: string, enum: [Yes, No]} + legally_allowed_to_work_in_us: {type: string, enum: [Yes, No]} + requires_eu_sponsorship: {type: string, enum: [Yes, No]} + required: [eu_work_authorization, us_work_authorization, requires_us_visa, requires_us_sponsorship, requires_eu_visa, legally_allowed_to_work_in_eu, legally_allowed_to_work_in_us, requires_eu_sponsorship] + +work_preferences: + type: object + properties: + remote_work: {type: string, enum: [Yes, No]} + in_person_work: {type: string, enum: [Yes, No]} + open_to_relocation: {type: string, enum: [Yes, No]} + willing_to_complete_assessments: {type: string, enum: [Yes, No]} + willing_to_undergo_drug_tests: {type: string, enum: [Yes, No]} + willing_to_undergo_background_checks: {type: string, enum: [Yes, No]} + required: [remote_work, in_person_work, open_to_relocation, willing_to_complete_assessments, willing_to_undergo_drug_tests, willing_to_undergo_background_checks] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index f74a689c00362ab6724bec113a729791b031cce0..c473e7251426a7a8eb528daafe8dadc3b19202cf 100644 GIT binary patch delta 48 zcmbQodW?0$JSGiZ1}=syhGK?%hCCpd%#gv5%8<*D$Y2YECJcHEMhwP4(trT~2HgnA delta 7 OcmX@cI*)b3JSG4P2LitU diff --git a/resume_yaml_generator.py b/resume_yaml_generator.py new file mode 100644 index 0000000..00e6922 --- /dev/null +++ b/resume_yaml_generator.py @@ -0,0 +1,164 @@ +import argparse +import yaml +from openai import OpenAI +import os +from typing import Dict, Any +import tiktoken +import re +from jsonschema import validate, ValidationError + +def load_yaml(file_path: str) -> Dict[str, Any]: + with open(file_path, 'r') as file: + return yaml.safe_load(file) + +def load_resume_text(file_path: str) -> str: + with open(file_path, 'r') as file: + return file.read() + +def get_api_key() -> str: + secrets_path = os.path.join('data_folder', 'secrets.yaml') + if not os.path.exists(secrets_path): + raise FileNotFoundError(f"Secrets file not found at {secrets_path}") + + secrets = load_yaml(secrets_path) + api_key = secrets.get('openai_api_key') + if not api_key: + raise ValueError("OpenAI API key not found in secrets.yaml") + + return api_key + +def num_tokens_from_string(string: str, model: str) -> int: + encoding = tiktoken.encoding_for_model(model) + return len(encoding.encode(string)) + +def generate_yaml_from_resume(resume_text: str, schema: Dict[str, Any], api_key: str) -> str: + client = OpenAI(api_key=api_key) + + prompt = f""" + I'm sending you the content of a text-based resume. Your task is to interpret this content and generate a YAML file that conforms to the following schema structure. + The generated YAML should include all required fields and follow the structure defined in the schema. + + Pay special attention to the property attributes in the schema. These indicate the expected type and format for each field: + - 'type': Specifies the data type (e.g., string, object, array) + - 'format': Indicates a specific format for certain fields: + - 'date' format should be a valid date (e.g., YYYY-MM-DD) + - 'phone_prefix' format should be a valid country code with a '+' prefix (e.g., +1 for US) + - 'phone' format should be a valid phone number + - 'email' format should be a valid email address + - 'uri' format should be a valid URL + - 'enum': Provides a list of allowed values for a field + + Important instructions: + 1. Ensure that the YAML structure matches exactly with the provided schema. Use a dictionary structure that mirrors the schema. + 2. For all sections, if information is not explicitly provided in the resume, make a best guess based on the context of the resume. This is CRUCIAL for the following fields: + - languages: Infer from the resume content or make an educated guess. Use the 'enum' values for proficiency. + - interests: Deduce from the overall resume or related experiences. + - availability (notice_period): Provide a reasonable estimate (e.g., "2 weeks" or "1 month"). + - salary_expectations (salary_range_usd): Estimate based on experience level and industry standards. + - self_identification: Make reasonable assumptions based on the resume context. Use 'enum' values where provided. + - legal_authorization: Provide plausible values based on the resume information. Use 'Yes' or 'No' as per the 'enum' values. + - work_preferences: Infer from job history, skills, and overall resume tone. Use 'Yes' or 'No' as per the 'enum' values. + 3. For the fields mentioned in point 2, always provide a value. Do not leave them blank or omit them. + 4. For the 'key_responsibilities' field in 'experience_details', format the responsibilities as follows: + responsibility_1: "Description of first responsibility" + responsibility_2: "Description of second responsibility" + responsibility_3: "Description of third responsibility" + responsibility_4: "Description of fourth responsibility" + Continue this pattern for all responsibilities listed. + 5. In the 'experience_details' section, ensure that 'position' comes before 'company' in each entry. + 6. For the 'skills_acquired' field in 'experience_details', infer relevant skills based on the job responsibilities and industry. Do not leave this field empty. + 7. Make reasonable inferences for any missing dates, such as date_of_birth or employment dates, ensuring they follow the 'date' format. + 8. For array types (e.g., education_details, experience_details), ensure to include all required fields for each item as specified in the schema. + + Resume Text Content: + {resume_text} + + YAML Schema: + {yaml.dump(schema, default_flow_style=False)} + + Generate the YAML content that matches this schema based on the resume content provided, ensuring all format hints are followed and making educated guesses where necessary. Be sure to include best guesses for ALL fields, even if not explicitly mentioned in the resume. + Enclose your response in tags. Only include the YAML content within these tags, without any additional text or code block markers. + """ + + model = "gpt-3.5-turbo-16k" # This model has a 16k token limit + tokens = num_tokens_from_string(prompt, model) + max_tokens = min(16385 - tokens, 4000) # Ensure we don't exceed model's limit + + if tokens > 16385: + print(f"Warning: The input exceeds the model's context length. Tokens: {tokens}") + + response = client.chat.completions.create( + model=model, + messages=[ + {"role": "system", "content": "You are a helpful assistant that generates structured YAML content from resume files, paying close attention to format requirements and schema structure."}, + {"role": "user", "content": prompt} + ], + max_tokens=max_tokens, + n=1, + stop=None, + temperature=0.5, + ) + + yaml_content = response.choices[0].message.content.strip() + + # Extract YAML content from between the tags + match = re.search(r'(.*?)', yaml_content, re.DOTALL) + if match: + return match.group(1).strip() + else: + raise ValueError("YAML content not found in the expected format") + +def save_yaml(data: str, output_file: str): + with open(output_file, 'w') as file: + file.write(data) + +def validate_yaml(yaml_content: str, schema: Dict[str, Any]) -> Dict[str, Any]: + try: + yaml_dict = yaml.safe_load(yaml_content) + validate(instance=yaml_dict, schema=schema) + return {"valid": True, "errors": None} + except ValidationError as e: + return {"valid": False, "errors": str(e)} + +def generate_report(validation_result: Dict[str, Any], output_file: str): + report = f"Validation Report for {output_file}\n" + report += "=" * 40 + "\n" + if validation_result["valid"]: + report += "YAML is valid and conforms to the schema.\n" + else: + report += "YAML is not valid. Errors:\n" + report += validation_result["errors"] + "\n" + + print(report) + with open(f"{output_file}_validation_report.txt", 'w') as file: + file.write(report) + +def main(): + parser = argparse.ArgumentParser(description="Generate a resume YAML file from a text resume using OpenAI API") + parser.add_argument("resume_file", help="Path to the input text resume file") + parser.add_argument("schema_file", help="Path to the YAML schema file") + parser.add_argument("output_file", help="Path to the output YAML file") + args = parser.parse_args() + + try: + api_key = get_api_key() + schema = load_yaml(args.schema_file) + resume_text = load_resume_text(args.resume_file) + + generated_yaml = generate_yaml_from_resume(resume_text, schema, api_key) + save_yaml(generated_yaml, args.output_file) + + print(f"Resume YAML generated and saved to {args.output_file}") + + validation_result = validate_yaml(generated_yaml, schema) + generate_report(validation_result, args.output_file) + + except FileNotFoundError as e: + print(f"Error: {e}") + except ValueError as e: + print(f"Error: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file From 93691913f99d2b432f1a733b6ee8975a29ddaa34 Mon Sep 17 00:00:00 2001 From: Maurice McCabe Date: Sat, 31 Aug 2024 15:41:08 -0700 Subject: [PATCH 05/41] update cmdline utility to match README --- {assets => data_folder_example}/resume_liam_murphy.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {assets => data_folder_example}/resume_liam_murphy.txt (100%) diff --git a/assets/resume_liam_murphy.txt b/data_folder_example/resume_liam_murphy.txt similarity index 100% rename from assets/resume_liam_murphy.txt rename to data_folder_example/resume_liam_murphy.txt From 67543499ab881909d3f5496d6c6404235953fdcc Mon Sep 17 00:00:00 2001 From: Maurice McCabe Date: Sat, 31 Aug 2024 15:41:33 -0700 Subject: [PATCH 06/41] update cmdline utility to match README --- .gitignore | 2 -- resume_yaml_generator.py | 27 ++++++++++++--------------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 1e8e081..4e73720 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,3 @@ generated_cv* .vscode chrome_profile answers.json -resume.yaml -resume.yaml_validation_report.txt diff --git a/resume_yaml_generator.py b/resume_yaml_generator.py index 00e6922..dcc6607 100644 --- a/resume_yaml_generator.py +++ b/resume_yaml_generator.py @@ -130,35 +130,32 @@ def generate_report(validation_result: Dict[str, Any], output_file: str): report += validation_result["errors"] + "\n" print(report) - with open(f"{output_file}_validation_report.txt", 'w') as file: - file.write(report) def main(): parser = argparse.ArgumentParser(description="Generate a resume YAML file from a text resume using OpenAI API") - parser.add_argument("resume_file", help="Path to the input text resume file") - parser.add_argument("schema_file", help="Path to the YAML schema file") - parser.add_argument("output_file", help="Path to the output YAML file") + parser.add_argument("--input", required=True, help="Path to the input text resume file") + parser.add_argument("--output", default="data_folder/plain_text_resume.yaml", help="Path to the output YAML file") args = parser.parse_args() try: api_key = get_api_key() - schema = load_yaml(args.schema_file) - resume_text = load_resume_text(args.resume_file) + schema = load_yaml("assets/resume_schema.yaml") + resume_text = load_resume_text(args.input) generated_yaml = generate_yaml_from_resume(resume_text, schema, api_key) - save_yaml(generated_yaml, args.output_file) + save_yaml(generated_yaml, args.output) - print(f"Resume YAML generated and saved to {args.output_file}") + print(f"Resume YAML generated and saved to {args.output}") validation_result = validate_yaml(generated_yaml, schema) - generate_report(validation_result, args.output_file) + if validation_result["valid"]: + print("YAML is valid and conforms to the schema.") + else: + print("YAML is not valid. Errors:") + print(validation_result["errors"]) - except FileNotFoundError as e: - print(f"Error: {e}") - except ValueError as e: - print(f"Error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + print(f"An error occurred: {e}") if __name__ == "__main__": main() \ No newline at end of file From 369c23791da7ffeb789c66f1e54a3fce9bcf28e2 Mon Sep 17 00:00:00 2001 From: Maurice McCabe Date: Sat, 31 Aug 2024 15:58:55 -0700 Subject: [PATCH 07/41] use gpt-4o-mini --- resume_yaml_generator.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/resume_yaml_generator.py b/resume_yaml_generator.py index dcc6607..0252caa 100644 --- a/resume_yaml_generator.py +++ b/resume_yaml_generator.py @@ -79,23 +79,13 @@ def generate_yaml_from_resume(resume_text: str, schema: Dict[str, Any], api_key: Generate the YAML content that matches this schema based on the resume content provided, ensuring all format hints are followed and making educated guesses where necessary. Be sure to include best guesses for ALL fields, even if not explicitly mentioned in the resume. Enclose your response in tags. Only include the YAML content within these tags, without any additional text or code block markers. """ - - model = "gpt-3.5-turbo-16k" # This model has a 16k token limit - tokens = num_tokens_from_string(prompt, model) - max_tokens = min(16385 - tokens, 4000) # Ensure we don't exceed model's limit - - if tokens > 16385: - print(f"Warning: The input exceeds the model's context length. Tokens: {tokens}") response = client.chat.completions.create( - model=model, + model="gpt-4o-mini", messages=[ {"role": "system", "content": "You are a helpful assistant that generates structured YAML content from resume files, paying close attention to format requirements and schema structure."}, {"role": "user", "content": prompt} ], - max_tokens=max_tokens, - n=1, - stop=None, temperature=0.5, ) @@ -106,8 +96,7 @@ def generate_yaml_from_resume(resume_text: str, schema: Dict[str, Any], api_key: if match: return match.group(1).strip() else: - raise ValueError("YAML content not found in the expected format") - + raise ValueError("YAML content not found in the expected format") def save_yaml(data: str, output_file: str): with open(output_file, 'w') as file: file.write(data) From a9d9e13474b29b05e9f2c537b3dd34b091bb33c5 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 1 Sep 2024 11:59:43 +0200 Subject: [PATCH 08/41] Adapter class added --- README.md | 3 +- data_folder/config.yaml | 8 ++--- src/gpt.py | 79 +++++++++++++++++++++++++++++++++-------- 3 files changed, 70 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b396b3f..fc4f060 100644 --- a/README.md +++ b/README.md @@ -211,11 +211,12 @@ This file defines your job search parameters and bot behavior. Each section cont - Marketing ``` - `llm_model_type`: - - Choose the model type, supported: openai / ollama + - Choose the model type, supported: openai / ollama / claude - `llm_model`: - Choose the LLM model, currently supported: - openai: gpt-4o - ollama: llama2, mistral:v0.3 + - claude: any model - `llm_api_url`: - Link of the API endpoint for the LLM model diff --git a/data_folder/config.yaml b/data_folder/config.yaml index e28e58a..60647c0 100644 --- a/data_folder/config.yaml +++ b/data_folder/config.yaml @@ -40,7 +40,7 @@ companyBlacklist: titleBlacklist: - word1 - word2 - -llm_model_type: [openai / ollama] -llm_model: ['gpt-4o' / 'mistral:v0.3'] -llm_api_url: [https://api.pawan.krd/cosmosrp/v1', http://127.0.0.1:11434/] \ No newline at end of file + +llm_model_type: [openai / ollama / claude] +llm_model: ['gpt-4o' / 'mistral:v0.3' / anymodel] +llm_api_url: [https://api.pawan.krd/cosmosrp/v1' / http://127.0.0.1:11434/] \ No newline at end of file diff --git a/src/gpt.py b/src/gpt.py index bc71e8b..b4b66bc 100644 --- a/src/gpt.py +++ b/src/gpt.py @@ -3,6 +3,7 @@ import os import re import textwrap from datetime import datetime +from abc import ABC, abstractmethod from typing import Dict, List, Union from pathlib import Path from dotenv import load_dotenv @@ -11,17 +12,75 @@ from langchain_core.output_parsers import StrOutputParser from langchain_core.prompt_values import StringPromptValue from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI -from langchain_ollama import ChatOllama from Levenshtein import distance import src.strings as strings load_dotenv() +class AIModel(ABC): + @abstractmethod + def generate_response(self, prompt: str) -> str: + pass + +class OpenAIModel(AIModel): + def __init__(self, api_key: str, llm_model: str, llm_api_url: str): + from langchain_openai import ChatOpenAI + self.model = ChatOpenAI(model_name=llm_model, openai_api_key=api_key, + temperature=0.4, base_url=llm_api_url) + + def generate_response(self, prompt: str) -> str: + response = self.model.invoke(prompt) + return response.content + +class ClaudeModel(AIModel): + def __init__(self, api_key: str, llm_model: str, llm_api_url: str): + from anthropic import Anthropic + self.client = Anthropic(api_key=api_key) + + def generate_response(self, prompt: str) -> str: + formatted_prompt = f"\n\nHuman: {prompt}\n\nAssistant:" + response = self.client.completions.create( + model="claude-2", + prompt=formatted_prompt, + max_tokens_to_sample=300 + ) + return response.completion.strip() + +class OllamaModel(AIModel): + def __init__(self, api_key: str, llm_model: str, llm_api_url: str): + from langchain_ollama import ChatOllama + self.model = ChatOllama(model=llm_model, base_url=llm_api_url) + + def generate_response(self, prompt: str) -> str: + response = self.model.invoke(prompt) + return response.content + +class AIAdapter: + def __init__(self, config: dict, api_key: str): + self.model = self._create_model(config, api_key) + + def _create_model(self, config: dict, api_key: str) -> AIModel: + llm_model_type = config['llm_model_type'] + llm_model = config['llm_model'] + llm_api_url = config['llm_api_url'] + print('Using {0} with {1} from {2}'.format(llm_model_type, llm_model, llm_api_url)) + + if llm_model_type == "openai": + return OpenAIModel(api_key, llm_model, llm_api_url) + elif llm_model_type == "claude": + return ClaudeModel(api_key, llm_model, llm_api_url) + elif llm_model_type == "ollama": + return OllamaModel(api_key, llm_model, llm_api_url) + else: + raise ValueError(f"Unsupported model type: {model_type}") + + def generate_response(self, prompt: str) -> str: + return self.model.generate_response(prompt) class LLMLogger: - def __init__(self, llm: Union[ChatOpenAI, ChatOllama]): + def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]): self.llm = llm @staticmethod @@ -79,7 +138,7 @@ class LLMLogger: class LoggerChatModel: - def __init__(self, llm: Union[ChatOpenAI, ChatOllama]): + def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]): self.llm = llm def __call__(self, messages: List[Dict[str, str]]) -> str: @@ -115,18 +174,8 @@ class LoggerChatModel: class GPTAnswerer: def __init__(self, config, llm_api_key): - llm_model_type = config['llm_model_type'] - llm_model = config['llm_model'] - llm_api_url = config['llm_api_url'] - - print('Using {0} with {1} from {2}'.format(llm_model_type, llm_model, llm_api_url)) - - if llm_model_type == "ollama": - self.llm_model = ChatOllama(model=llm_model, temperature = 0.4, base_url=llm_api_url) - elif llm_model_type == "openai": - self.llm_model = ChatOpenAI(model_name=llm_model, openai_api_key=llm_api_key, temperature=0.4, - base_url=llm_api_url) - self.llm_cheap = LoggerChatModel(self.llm_model) + self.ai_adapter = AIAdapter(config, llm_api_key) + self.llm_cheap = LoggerChatModel(self.ai_adapter) @property def job_description(self): return self.job.description From 6b4ebe1bd0546771b2e105788ab6a9f759377633 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 1 Sep 2024 12:01:39 +0200 Subject: [PATCH 09/41] config.yaml corrected --- data_folder/config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_folder/config.yaml b/data_folder/config.yaml index 60647c0..f38ce82 100644 --- a/data_folder/config.yaml +++ b/data_folder/config.yaml @@ -42,5 +42,5 @@ titleBlacklist: - word2 llm_model_type: [openai / ollama / claude] -llm_model: ['gpt-4o' / 'mistral:v0.3' / anymodel] +llm_model: [gpt-4o / mistral:v0.3 / anymodel] llm_api_url: [https://api.pawan.krd/cosmosrp/v1' / http://127.0.0.1:11434/] \ No newline at end of file From 9ed0c3ea6385d16177703fb3fe9ed45d9fb5cc5d Mon Sep 17 00:00:00 2001 From: Earl Perry Date: Sun, 1 Sep 2024 09:01:52 -0400 Subject: [PATCH 10/41] Instead of adding a documentation section, improved trouble shooting guide, with current issues, solutions, and addtional resources --- README.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 15fe7b8..adb4eb1 100644 --- a/README.md +++ b/README.md @@ -501,19 +501,84 @@ Using this folder as a guide can be particularly helpful for: python main.py --resume /path/to/your/resume.pdf ``` -## Documentation -TODO ): +### Troubleshooting Common Issues -## Troubleshooting +#### 1. OpenAI API Rate Limit Errors + +**Error Message:** + +openai.RateLimitError: Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}} + +**Solution:** +- Check your OpenAI API billing settings at https://platform.openai.com/account/billing +- Ensure you have added a valid payment method to your OpenAI account +- Note that ChatGPT Plus subscription is different from API access +- If you've recently added funds or upgraded, wait 12-24 hours for changes to take effect +- Free tier has a 3 RPM limit; spend at least $5 on API usage to increase + +#### 2. LinkedIn Easy Apply Button Not Found + +**Error Message:** + +Exception: No clickable 'Easy Apply' button found + +**Solution:** +- Ensure that you're logged into LinkedIn properly +- Check if the job listings you're targeting actually have the "Easy Apply" option +- Verify that your search parameters in the `config.yaml` file are correct and returning jobs with the "Easy Apply" button +- Try increasing the wait time for page loading in the script to ensure all elements are loaded before searching for the button + +#### 3. Incorrect Information in Job Applications + +**Issue:** Bot provides inaccurate data for experience, CTC, and notice period + +**Solution:** +- Update prompts for professional experience specificity +- Add fields in `config.yaml` for current CTC, expected CTC, and notice period +- Modify bot logic to use these new config fields + +#### 4. YAML Configuration Errors + +**Error Message:** + +yaml.scanner.ScannerError: while scanning a simple key + +**Solution:** +- Copy example `config.yaml` and modify gradually +- Ensure proper YAML indentation and spacing +- Use a YAML validator tool +- Avoid unnecessary special characters or quotes + +#### 5. Bot Logs In But Doesn't Apply to Jobs + +**Issue:** Bot searches for jobs but continues scrolling without applying + +**Solution:** +- Check for security checks or CAPTCHAs +- Verify `config.yaml` job search parameters +- Ensure your LinkedIn profile meets job requirements +- Review console output for error messages + +### General Troubleshooting Tips + +- Use the latest version of the script +- Verify all dependencies are installed and updated +- Check internet connection stability +- Use VPNs cautiously to avoid triggering LinkedIn security +- Clear browser cache and cookies if issues persist + +For further assistance, please create an issue on the [GitHub repository](https://github.com/feder-cr/LinkedIn_AIHawk_automatic_job_application/issues) with detailed information about your problem, including error messages and your configuration (with sensitive information removed). + +### Additional Resources + +- [Video Tutorial: How to set up LinkedIn_AIHawk](https://youtu.be/gdW9wogHEUM) +- [OpenAI API Documentation](https://platform.openai.com/docs/) +- [LinkedIn Developer Documentation](https://developer.linkedin.com/) +- [Lang Chain Developer Documentation](https://python.langchain.com/v0.2/docs/integrations/components/) + +Remember to always use LinkedIn_AIHawk responsibly and in compliance with LinkedIn's terms of service. -- **Carefully read logs and output :** Most of the errors are verbosely reflected just watch the output and try to find the root couse. -- **If nothing works by unknown reason:** Use tested OS. Reboot and/or update OS. Use new clean venv. Try update Python to the tested version. -- **ChromeDriver Issues:** Ensure ChromeDriver is compatible with your installed Chrome version. -- **Missing Files:** Verify that all necessary files are present in the data folder. -- **Invalid YAML:** Check your YAML files for syntax errors . Try to use external YAML validators e.g. https://www.yamllint.com/ -- **OpenAI endpoint isues**: Try to check possible limits\blocking at their side - If you encounter any issues, you can open an issue on [GitHub](https://github.com/feder-cr/linkedIn_auto_jobs_applier_with_AI/issues). Please add valuable details to the subject and to the description. If you need new feature then please reflect this. I'll be more than happy to assist you! From 777d1fa4a6cd94ecf6805c671eacda85bec1d28f Mon Sep 17 00:00:00 2001 From: user Date: Sun, 1 Sep 2024 19:33:42 +0200 Subject: [PATCH 11/41] Claude support with langchain antrhopic --- data_folder/config.yaml | 4 ++-- requirements.txt | Bin 674 -> 766 bytes src/gpt.py | 31 ++++++++++++++----------------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/data_folder/config.yaml b/data_folder/config.yaml index f38ce82..d26a6cc 100644 --- a/data_folder/config.yaml +++ b/data_folder/config.yaml @@ -42,5 +42,5 @@ titleBlacklist: - word2 llm_model_type: [openai / ollama / claude] -llm_model: [gpt-4o / mistral:v0.3 / anymodel] -llm_api_url: [https://api.pawan.krd/cosmosrp/v1' / http://127.0.0.1:11434/] \ No newline at end of file +llm_model: [gpt-4o / mistral:v0.3 / claude-3-5-sonnet-20240620] +llm_api_url: [https://api.pawan.krd/cosmosrp/v1', http://127.0.0.1:11434/, https://api.anthropic.com/v1/messages] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index aef4baed9ae147969488a70f26fd5aae9edacb36..0edf5dfd3cbcff11444a30bf3b3f85ae1798dc54 100644 GIT binary patch delta 57 zcmZ3)`j2(OEy;X_93V_&$OU3sAT(gmV=x3_XG3844IO8Im_1 He8>m@q~8vv delta 11 Scmeyzx`=hct<7ePCl~=A_youR diff --git a/src/gpt.py b/src/gpt.py index b4b66bc..22c7d6c 100644 --- a/src/gpt.py +++ b/src/gpt.py @@ -20,7 +20,7 @@ load_dotenv() class AIModel(ABC): @abstractmethod - def generate_response(self, prompt: str) -> str: + def invoke(self, prompt: str) -> str: pass class OpenAIModel(AIModel): @@ -29,32 +29,29 @@ class OpenAIModel(AIModel): self.model = ChatOpenAI(model_name=llm_model, openai_api_key=api_key, temperature=0.4, base_url=llm_api_url) - def generate_response(self, prompt: str) -> str: + def invoke(self, prompt: str) -> str: + print("invoke in openai") response = self.model.invoke(prompt) - return response.content + return response class ClaudeModel(AIModel): def __init__(self, api_key: str, llm_model: str, llm_api_url: str): - from anthropic import Anthropic - self.client = Anthropic(api_key=api_key) + from langchain_anthropic import ChatAnthropic + self.model = ChatAnthropic(model=llm_model, api_key=api_key, + temperature=0.4, base_url=llm_api_url) - def generate_response(self, prompt: str) -> str: - formatted_prompt = f"\n\nHuman: {prompt}\n\nAssistant:" - response = self.client.completions.create( - model="claude-2", - prompt=formatted_prompt, - max_tokens_to_sample=300 - ) - return response.completion.strip() + def invoke(self, prompt: str) -> str: + response = self.model.invoke(prompt) + return response class OllamaModel(AIModel): def __init__(self, api_key: str, llm_model: str, llm_api_url: str): from langchain_ollama import ChatOllama self.model = ChatOllama(model=llm_model, base_url=llm_api_url) - def generate_response(self, prompt: str) -> str: + def invoke(self, prompt: str) -> str: response = self.model.invoke(prompt) - return response.content + return response class AIAdapter: def __init__(self, config: dict, api_key: str): @@ -75,8 +72,8 @@ class AIAdapter: else: raise ValueError(f"Unsupported model type: {model_type}") - def generate_response(self, prompt: str) -> str: - return self.model.generate_response(prompt) + def invoke(self, prompt: str) -> str: + return self.model.invoke(prompt) class LLMLogger: From bb4e0ed8a6fd841083fd6dcdff48c5ffbd0708fa Mon Sep 17 00:00:00 2001 From: anton6tak Date: Sun, 1 Sep 2024 10:49:36 -0700 Subject: [PATCH 12/41] add information about request limit errors and account type issues --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 15fe7b8..1373357 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,11 @@ This file contains sensitive information. Never share or commit this file to ver - Replace with your OpenAI API key for GPT integration - To obtain an API key, follow the tutorial at: https://medium.com/@lorenzozar/how-to-get-your-own-openai-api-key-f4d44e60c327 - Note: You need to add credit to your OpenAI account to use the API. You can add credit by visiting the [OpenAI billing dashboard](https://platform.openai.com/account/billing). + - According to the [OpenAI community](https://community.openai.com/t/usage-tier-free-to-tier-1/919150) and our users' reports, right after setting up the OpenAI account and purchasing the required credits, users still have a `Free` account type. This prevents them from having unlimited access to OpenAI models and allows only 200 requests per day. This might cause runtime errors such as: + `Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. ...}}` + `{'error': {'message': 'Rate limit reached for gpt-4o-mini in organization on requests per day (RPD): Limit 200, Used 200, Requested 1.}}` + OpenAI will update your account automatically, but it might take some time, ranging from a couple of hours to a few days. + You can find more about your organization limits on the [official page](https://platform.openai.com/settings/organization/limits). From 28b8fa37469fab8b09b454911f967c8e5be0a825 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 1 Sep 2024 21:15:03 +0200 Subject: [PATCH 13/41] README updated --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fc4f060..cba2849 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,9 @@ This file defines your job search parameters and bot behavior. Each section cont - Sales - Marketing ``` -- `llm_model_type`: +#### 2.1 config.yaml - Customize LLM model endpoint + +- `llm_model_type`: - Choose the model type, supported: openai / ollama / claude - `llm_model`: - Choose the LLM model, currently supported: @@ -219,7 +221,11 @@ This file defines your job search parameters and bot behavior. Each section cont - claude: any model - `llm_api_url`: - Link of the API endpoint for the LLM model - + - openai: https://api.pawan.krd/cosmosrp/v1 + - ollama: http://127.0.0.1:11434/ + - claude: https://api.anthropic.com/v1 + - Note: To run local Ollama, follow the guidelines here: [Guide to Ollama deployment](https://github.com/ollama/ollama) + ### 3. plain_text_resume.yaml This file contains your resume information in a structured format. Fill it out with your personal details, education, work experience, and skills. This information is used to auto-fill application forms and generate customized resumes. From d1d9e9f6a3eedbe4a0399f632559a4a332b740e7 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 1 Sep 2024 21:16:02 +0200 Subject: [PATCH 14/41] config yaml file with default openai gpt freely hosted --- data_folder/config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data_folder/config.yaml b/data_folder/config.yaml index d26a6cc..bfbcd82 100644 --- a/data_folder/config.yaml +++ b/data_folder/config.yaml @@ -41,6 +41,6 @@ titleBlacklist: - word1 - word2 -llm_model_type: [openai / ollama / claude] -llm_model: [gpt-4o / mistral:v0.3 / claude-3-5-sonnet-20240620] -llm_api_url: [https://api.pawan.krd/cosmosrp/v1', http://127.0.0.1:11434/, https://api.anthropic.com/v1/messages] \ No newline at end of file +llm_model_type: openai +llm_model: gpt-4o +llm_api_url: https://api.pawan.krd/cosmosrp/v1 \ No newline at end of file From c0f186ba844f7b12ea515675709c4977bc817e32 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sun, 1 Sep 2024 22:48:01 +0200 Subject: [PATCH 15/41] Fixed easy apply search, implemented method to extract required fields from API with new endpoint, Todo; parse result json to return a dict that contains title, optionUrn and possible choices for the value if present --- src/linkedin-api.py | 76 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/src/linkedin-api.py b/src/linkedin-api.py index f1395f8..6bb2c12 100644 --- a/src/linkedin-api.py +++ b/src/linkedin-api.py @@ -1,12 +1,19 @@ from typing import Dict, List from linkedin_api import Linkedin from typing import Optional, Union, Literal -from urllib.parse import urlencode +from urllib.parse import quote, urlencode +import logging +import json + +# set log to all debug +logging.basicConfig(level=logging.INFO) + class LinkedInEvolvedAPI(Linkedin): def __init__(self, username, password): super().__init__(username, password) + def search_jobs( self, keywords: Optional[str] = None, @@ -106,7 +113,7 @@ class LinkedInEvolvedAPI(Linkedin): if remote: query["selectedFilters"]["workplaceType"] = f"List({','.join(remote)})" if easy_apply: - query["selectedFilters"]["easyApply"] = "List(true)" + query["selectedFilters"]["applyWithLinkedin"] = "List(true)" query["selectedFilters"]["timePostedRange"] = f"List(r{listed_at})" query["spellCorrectionEnabled"] = "true" @@ -160,9 +167,72 @@ class LinkedInEvolvedAPI(Linkedin): self.logger.debug(f"results grew to {len(results)}") return results - + def get_fields_for_easy_apply(self,job_id:str) -> List[Dict]: + """Get fields needed for easy apply jobs. + + :param job_id: Job ID + :type job_id: str + :return: Fields + :rtype: dict + """ + + cookies = self.client.session.cookies.get_dict() + cookie_str = "; ".join([f"{k}={v}" for k, v in cookies.items()]) + + headers: Dict[str, str] = self._headers() + + headers['User-Agent'] = headers['user-agent'] + headers['Accept-Language'] = headers['accept-language'] + headers["Accept"] = "application/vnd.linkedin.normalized+json+2.1" + headers["csrf-token"] = cookies["JSESSIONID"].replace('"', "") + headers["Cookie"] = cookie_str + headers["Connection"] = "keep-alive" + + headers.pop("user-agent") + headers.pop("accept-language") + default_params = { + "decorationId": "com.linkedin.voyager.dash.deco.jobs.OnsiteApplyApplication-67", + "jobPostingUrn": f"urn:li:fsd_jobPosting:{job_id}", + "q": "jobPosting", + } + + default_params = urlencode(default_params) + res = self._fetch( + f"/voyagerJobsDashOnsiteApplyApplication?{default_params}", + headers=headers, + cookies=cookies, + ) + + try: + data = res.json() + except ValueError: + self.logger.error("Failed to parse JSON response") + return [] + + form_components = [] + + for item in data.get("included", []): + if 'formComponent' in item: + title = item['title']['text'] + form_components.append({title: item['formComponent']}) + + + + + def get_cookies_hitting_url(self, url: str): + res = self._fetch(url,base_request=False) + return res.headers + +## EXAMPLE USAGE +#if __name__ == "__main__": +# api: LinkedInEvolvedAPI = LinkedInEvolvedAPI("", "") +# jobs = api.search_jobs(keywords="Python", location_name="Italy", limit=1, easy_apply=True) +# for job in jobs: +# job_id: str = job["job_id"] +# fields = api.get_fields_for_easy_apply(job_id) + \ No newline at end of file From d76a35fa7ca0d53f93035ca8ed5e2f314b5b424d Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sun, 1 Sep 2024 22:53:05 +0200 Subject: [PATCH 16/41] Fixed easy apply search, implemented method to extract required fields from API with new endpoint, Todo; parse result json to return a dict that contains title, optionUrn and possible choices for the value if present --- src/linkedin-api.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/linkedin-api.py b/src/linkedin-api.py index 6bb2c12..8b642ce 100644 --- a/src/linkedin-api.py +++ b/src/linkedin-api.py @@ -222,10 +222,6 @@ class LinkedInEvolvedAPI(Linkedin): - def get_cookies_hitting_url(self, url: str): - res = self._fetch(url,base_request=False) - return res.headers - ## EXAMPLE USAGE #if __name__ == "__main__": # api: LinkedInEvolvedAPI = LinkedInEvolvedAPI("", "") From ff087129b09bb21635a106e639d5ffd173f1dd4e Mon Sep 17 00:00:00 2001 From: Maurice McCabe Date: Mon, 2 Sep 2024 01:06:11 -0700 Subject: [PATCH 17/41] add support for converting a pdf to txt --- requirements.txt | 3 ++- resume_yaml_generator.py | 29 ++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7f6e144..5139ade 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,4 +12,5 @@ selenium==4.9.1 webdriver-manager==4.0.2 click git+https://github.com/feder-cr/lib_resume_builder_AIHawk.git -linkedin-api \ No newline at end of file +linkedin-api +PyPDF2==3.0.1 \ No newline at end of file diff --git a/resume_yaml_generator.py b/resume_yaml_generator.py index 0252caa..acf36f5 100644 --- a/resume_yaml_generator.py +++ b/resume_yaml_generator.py @@ -3,9 +3,9 @@ import yaml from openai import OpenAI import os from typing import Dict, Any -import tiktoken import re from jsonschema import validate, ValidationError +import PyPDF2 def load_yaml(file_path: str) -> Dict[str, Any]: with open(file_path, 'r') as file: @@ -27,10 +27,6 @@ def get_api_key() -> str: return api_key -def num_tokens_from_string(string: str, model: str) -> int: - encoding = tiktoken.encoding_for_model(model) - return len(encoding.encode(string)) - def generate_yaml_from_resume(resume_text: str, schema: Dict[str, Any], api_key: str) -> str: client = OpenAI(api_key=api_key) @@ -96,7 +92,8 @@ def generate_yaml_from_resume(resume_text: str, schema: Dict[str, Any], api_key: if match: return match.group(1).strip() else: - raise ValueError("YAML content not found in the expected format") + raise ValueError("YAML content not found in the expected format") + def save_yaml(data: str, output_file: str): with open(output_file, 'w') as file: file.write(data) @@ -120,16 +117,30 @@ def generate_report(validation_result: Dict[str, Any], output_file: str): print(report) +def pdf_to_text(pdf_path: str) -> str: + text = "" + with open(pdf_path, 'rb') as file: + reader = PyPDF2.PdfReader(file) + for page in reader.pages: + text += page.extract_text() + return text + def main(): - parser = argparse.ArgumentParser(description="Generate a resume YAML file from a text resume using OpenAI API") - parser.add_argument("--input", required=True, help="Path to the input text resume file") + parser = argparse.ArgumentParser(description="Generate a resume YAML file from a PDF or text resume using OpenAI API") + parser.add_argument("--input", required=True, help="Path to the input resume file (PDF or TXT)") parser.add_argument("--output", default="data_folder/plain_text_resume.yaml", help="Path to the output YAML file") args = parser.parse_args() try: api_key = get_api_key() schema = load_yaml("assets/resume_schema.yaml") - resume_text = load_resume_text(args.input) + + # Check if input is PDF or TXT + if args.input.lower().endswith('.pdf'): + resume_text = pdf_to_text(args.input) + print(f"PDF resume converted to text successfully.") + else: + resume_text = load_resume_text(args.input) generated_yaml = generate_yaml_from_resume(resume_text, schema, api_key) save_yaml(generated_yaml, args.output) From 23567ee7c48618d10a5acdc14665b16321a7104f Mon Sep 17 00:00:00 2001 From: Maurice McCabe Date: Mon, 2 Sep 2024 01:25:38 -0700 Subject: [PATCH 18/41] updated for generating resume yaml from pdf --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 76621d7..edb4dc8 100644 --- a/README.md +++ b/README.md @@ -452,21 +452,21 @@ Each section has specific fields to fill out: willing_to_undergo_drug_tests: "No" willing_to_undergo_background_checks: "Yes" ``` -### 4. Generating plain_text_resume.yaml from a Text Resume +### 4. Generating plain_text_resume.yaml from a PDF or Text Resume -To simplify the process of creating your `plain_text_resume.yaml` file, you can use the provided script to generate it from a text-based resume. Follow these steps: +To simplify the process of creating your `plain_text_resume.yaml` file, you can use the provided script to generate it from a pdf-based or text-based resume. Follow these steps: -1. Prepare your resume in a plain text format (.txt file). +1. Prepare your resume in a pdf (.pdf file) or plain text (.txt file) format. -2. Place your text resume in the `data_folder` directory. +2. Place your resume in the `data_folder` directory. 3. Run the following command: ```bash - python generate_resume_yaml.py --input data_folder/your_resume.txt --output data_folder/plain_text_resume.yaml + python generate_resume_yaml.py --input data_folder/your_resume.[pdf|txt] --output data_folder/plain_text_resume.yaml ``` - Replace `your_resume.txt` with the actual name of your text resume file. + Replace `your_resume.[pdf|txt]` with the actual name of your pdf or text resume file. 4. The script will generate a `plain_text_resume.yaml` file in the `data_folder` directory. From 685da0f9fc71dbb7a0c869efa2f702bab1cdcc1e Mon Sep 17 00:00:00 2001 From: Maurice McCabe Date: Mon, 2 Sep 2024 01:44:54 -0700 Subject: [PATCH 19/41] replace lib PYPDF2 with pdfminer.six --- requirements.txt | 32 ++++++++++++++++---------------- resume_yaml_generator.py | 9 ++------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5139ade..03290b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ -langchain==0.2.11 -langchain-community==0.2.10 -langchain-core==0.2.24 -langchain-openai==0.1.17 -langchain-text-splitters==0.2.2 -langsmith==0.1.93 -Levenshtein==0.25.1 -openai==1.37.1 -regex==2024.7.24 -reportlab==4.2.2 -selenium==4.9.1 -webdriver-manager==4.0.2 -click -git+https://github.com/feder-cr/lib_resume_builder_AIHawk.git -linkedin-api -PyPDF2==3.0.1 \ No newline at end of file +langchain==0.2.11 +langchain-community==0.2.10 +langchain-core==0.2.24 +langchain-openai==0.1.17 +langchain-text-splitters==0.2.2 +langsmith==0.1.93 +Levenshtein==0.25.1 +openai==1.37.1 +regex==2024.7.24 +reportlab==4.2.2 +selenium==4.9.1 +webdriver-manager==4.0.2 +click +git+https://github.com/feder-cr/lib_resume_builder_AIHawk.git +linkedin-api +pdfminer.six==20221105 \ No newline at end of file diff --git a/resume_yaml_generator.py b/resume_yaml_generator.py index acf36f5..46982c2 100644 --- a/resume_yaml_generator.py +++ b/resume_yaml_generator.py @@ -5,7 +5,7 @@ import os from typing import Dict, Any import re from jsonschema import validate, ValidationError -import PyPDF2 +from pdfminer.high_level import extract_text def load_yaml(file_path: str) -> Dict[str, Any]: with open(file_path, 'r') as file: @@ -118,12 +118,7 @@ def generate_report(validation_result: Dict[str, Any], output_file: str): print(report) def pdf_to_text(pdf_path: str) -> str: - text = "" - with open(pdf_path, 'rb') as file: - reader = PyPDF2.PdfReader(file) - for page in reader.pages: - text += page.extract_text() - return text + return extract_text(pdf_path) def main(): parser = argparse.ArgumentParser(description="Generate a resume YAML file from a PDF or text resume using OpenAI API") From d2372523fa876c3abbceffd9c7b371a3afb10535 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Mon, 2 Sep 2024 11:10:18 +0200 Subject: [PATCH 20/41] fixed the parsing of the required fields for apply --- src/linkedin-api.py | 66 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/src/linkedin-api.py b/src/linkedin-api.py index 8b642ce..a34e381 100644 --- a/src/linkedin-api.py +++ b/src/linkedin-api.py @@ -13,7 +13,6 @@ class LinkedInEvolvedAPI(Linkedin): def __init__(self, username, password): super().__init__(username, password) - def search_jobs( self, keywords: Optional[str] = None, @@ -182,16 +181,12 @@ class LinkedInEvolvedAPI(Linkedin): headers: Dict[str, str] = self._headers() - headers['User-Agent'] = headers['user-agent'] - headers['Accept-Language'] = headers['accept-language'] + headers["Accept"] = "application/vnd.linkedin.normalized+json+2.1" headers["csrf-token"] = cookies["JSESSIONID"].replace('"', "") headers["Cookie"] = cookie_str headers["Connection"] = "keep-alive" - headers.pop("user-agent") - headers.pop("accept-language") - default_params = { "decorationId": "com.linkedin.voyager.dash.deco.jobs.OnsiteApplyApplication-67", @@ -206,6 +201,16 @@ class LinkedInEvolvedAPI(Linkedin): cookies=cookies, ) + match res.status_code: + case 200: + pass + case 409: + self.logger.error("Failed to fetch fields for easy apply job because already applied to this job!") + return [] + case _: + self.logger.error("Failed to fetch fields for easy apply job") + return [] + try: data = res.json() except ValueError: @@ -216,19 +221,48 @@ class LinkedInEvolvedAPI(Linkedin): for item in data.get("included", []): if 'formComponent' in item: - title = item['title']['text'] - form_components.append({title: item['formComponent']}) + urn = item['urn'] + try: + title = item['title']['text'] + except TypeError: + title = urn + + form_component_type = list(item['formComponent'].keys())[0] + form_component_details = item['formComponent'][form_component_type] + + component_info = { + 'title': title, + 'urn': urn, + 'formComponentType': form_component_type, + } + + if 'textSelectableOptions' in form_component_details: + options = [ + opt['optionText']['text'] for opt in form_component_details['textSelectableOptions'] + ] + component_info['selectableOptions'] = options + elif 'selectableOptions' in form_component_details: + options = [ + opt['textSelectableOption']['optionText']['text'] + for opt in form_component_details['selectableOptions'] + ] + component_info['selectableOptions'] = options + + form_components.append(component_info) + return form_components - - ## EXAMPLE USAGE -#if __name__ == "__main__": -# api: LinkedInEvolvedAPI = LinkedInEvolvedAPI("", "") -# jobs = api.search_jobs(keywords="Python", location_name="Italy", limit=1, easy_apply=True) -# for job in jobs: -# job_id: str = job["job_id"] -# fields = api.get_fields_for_easy_apply(job_id) +if __name__ == "__main__": + api: LinkedInEvolvedAPI = LinkedInEvolvedAPI(username="", password="") + jobs = api.search_jobs(keywords="Frontend Developer", location_name="Italia", limit=5, easy_apply=True, offset=1) + for job in jobs: + job_id: str = job["job_id"] + + fields = api.get_fields_for_easy_apply(job_id) + for field in fields: + print(field) + \ No newline at end of file From 092f4e70324561304f24bef30af2599be5185bfb Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Tue, 3 Sep 2024 00:27:54 +0200 Subject: [PATCH 21/41] Added instructions toDo; about apply method --- src/linkedin-api.py | 115 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 111 insertions(+), 4 deletions(-) diff --git a/src/linkedin-api.py b/src/linkedin-api.py index a34e381..ae37845 100644 --- a/src/linkedin-api.py +++ b/src/linkedin-api.py @@ -8,7 +8,6 @@ import json # set log to all debug logging.basicConfig(level=logging.INFO) - class LinkedInEvolvedAPI(Linkedin): def __init__(self, username, password): super().__init__(username, password) @@ -167,7 +166,7 @@ class LinkedInEvolvedAPI(Linkedin): return results - def get_fields_for_easy_apply(self,job_id:str) -> List[Dict]: + def get_fields_for_easy_apply(self,job_id: str) -> List[Dict]: """Get fields needed for easy apply jobs. :param job_id: Job ID @@ -220,7 +219,7 @@ class LinkedInEvolvedAPI(Linkedin): form_components = [] for item in data.get("included", []): - if 'formComponent' in item: + if 'formComponent' in item: urn = item['urn'] try: title = item['title']['text'] @@ -251,10 +250,117 @@ class LinkedInEvolvedAPI(Linkedin): form_components.append(component_info) return form_components + + def apply_to_job(self,job_id: str, fields: dict, followCompany: bool = True) -> bool: + return False + + # ToDo: Implement apply to job parser first + # How need to be implemented: + # 1. Get fields for easy apply job from the previous method (get_fields_for_easy_apply) + # 2. Fill the fields with the data adding a response parameter in the specific field in the dict object, for example: + # {'title': 'Quanti anni di esperienza di lavoro hai con Router?', 'urn': 'urn:li:fsd_formElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4013860791,9478711764,numeric)', 'formComponentType': 'singleLineTextFormComponent'} + # Became: + # {'title': 'Quanti anni di esperienza di lavoro hai con Router?', 'urn': 'urn:li:fsd_formElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4013860791,9478711764,numeric)', 'formComponentType': 'singleLineTextFormComponent', 'response': '5'} + # To fill, you can temporary use input() function to get the data from the user manually for testing purposes (for the further implementation, the question will be asked to AI implementation and automatically filled) + # Build a working payload. + + # EXAMPLE OF WORKING PAYLOAD + # 4005350454 is job_id, so need to be replaced with the job_id + + #{ + # "followCompany": true, + # "responses": [ + # { + # "formElementUrn": "urn:li:fsd_formElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4005350454,3497278561,multipleChoice)", + # "formElementInputValues": [ + # { + # "entityInputValue": { + # "inputEntityName": "email@gmail.com" + # } + # } + # ] + # }, + # { + # "formElementUrn": "urn:li:fsd_formElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4005350454,3497278545,phoneNumber~country)", + # "formElementInputValues": [ + # { + # "entityInputValue": { + # "inputEntityName": "Italy (+39)", + # "inputEntityUrn": "urn:li:country:it" + # } + # } + # ] + # }, + # { + # "formElementUrn": "urn:li:fsd_formElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4005350454,3497278545,phoneNumber~nationalNumber)", + # "formElementInputValues": [ + # { + # "textInputValue": "3333333" + # } + # ] + # }, + # { + # "formElementUrn": "urn:li:fsd_formElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4005350454,3497278529,multipleChoice)", + # "formElementInputValues": [ + # { + # "entityInputValue": { + # "inputEntityName": "Native or bilingual" + # } + # } + # ] + # }, + # { + # "formElementUrn": "urn:li:fsd_formElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4005350454,3497278537,numeric)", + # "formElementInputValues": [ + # { + # "textInputValue": "0" + # } + # ] + # }, + # { + # "formElementUrn": "urn:li:fsd_formElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4005350454,3498546713,multipleChoice)", + # "formElementInputValues": [ + # { + # "entityInputValue": { + # "inputEntityName": "No" + # } + # } + # ] + # }, + # { + # "formElementUrn": "urn:li:fsd_formElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4005350454,3497278521,multipleChoice)", + # "formElementInputValues": [ + # { + # "entityInputValue": { + # "inputEntityName": "No" + # } + # } + # ] + # } + # ], + # "referenceId": "", + # "trackingCode": "d_flagship3_search_srp_jobs", + # "fileUploadResponses": [ + # { + # "inputUrn": "urn:li:fsd_resume:/##todo##", + # "formElementUrn": "urn:li:fsu_jobApplicationFileUploadFormElement:urn:li:jobs_applyformcommon_easyApplyFormElement:(4005350454,3497278553,document)" + # } + # ], + # "trackingId": "" + #} + + # Push the commit to the repository and create a pull request to the v3 branch. + + + + + + + ## EXAMPLE USAGE if __name__ == "__main__": - api: LinkedInEvolvedAPI = LinkedInEvolvedAPI(username="", password="") + api: LinkedInEvolvedAPI = LinkedInEvolvedAPI(username="", password="") jobs = api.search_jobs(keywords="Frontend Developer", location_name="Italia", limit=5, easy_apply=True, offset=1) for job in jobs: job_id: str = job["job_id"] @@ -262,6 +368,7 @@ if __name__ == "__main__": fields = api.get_fields_for_easy_apply(job_id) for field in fields: print(field) + break From ba7ac0fbb1994b95a9b5b96f3db92bd2881dc2eb Mon Sep 17 00:00:00 2001 From: Ritesh Date: Tue, 3 Sep 2024 12:45:06 +0530 Subject: [PATCH 22/41] handled already logined case --- src/linkedIn_authenticator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/linkedIn_authenticator.py b/src/linkedIn_authenticator.py index c953e5a..5f30378 100644 --- a/src/linkedIn_authenticator.py +++ b/src/linkedIn_authenticator.py @@ -25,6 +25,9 @@ class LinkedInAuthenticator: def handle_login(self): print("Navigating to the LinkedIn login page...") self.driver.get("https://www.linkedin.com/login") + if 'feed' in self.driver.current_url: + print("User is already logged in.") + return try: self.enter_credentials() self.submit_login_form() From 6d72788f807be86635d21acb5afd96469ff24294 Mon Sep 17 00:00:00 2001 From: feder-cr <85809106+feder-cr@users.noreply.github.com> Date: Tue, 3 Sep 2024 16:35:28 +0200 Subject: [PATCH 23/41] cover letter fixed --- .gitignore | 1 + src/linkedIn_easy_applier.py | 36 +++++++++--------------------------- src/linkedIn_job_manager.py | 12 ------------ 3 files changed, 10 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index 4e73720..6d06188 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ generated_cv* .vscode chrome_profile answers.json +data* \ No newline at end of file diff --git a/src/linkedIn_easy_applier.py b/src/linkedIn_easy_applier.py index 8c95d8c..c9b9625 100644 --- a/src/linkedIn_easy_applier.py +++ b/src/linkedIn_easy_applier.py @@ -30,7 +30,6 @@ class LinkedInEasyApplier: self.resume_generator_manager = resume_generator_manager self.all_data = self._load_questions_from_json() - def _load_questions_from_json(self) -> List[dict]: output_file = 'answers.json' try: @@ -49,7 +48,6 @@ class LinkedInEasyApplier: tb_str = traceback.format_exc() raise Exception(f"Error loading questions data from JSON file: \nTraceback:\n{tb_str}") - def job_apply(self, job: Any): self.driver.get(job.link) time.sleep(random.uniform(3, 5)) @@ -91,7 +89,6 @@ class LinkedInEasyApplier: 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"]') @@ -107,7 +104,6 @@ class LinkedInEasyApplier: tb_str = traceback.format_exc() raise Exception(f"Error getting Job description: \nTraceback:\n{tb_str}") - def _get_job_recruiter(self): try: hiring_team_section = WebDriverWait(self.driver, 10).until( @@ -253,16 +249,12 @@ class LinkedInEasyApplier: if radios: question_text = section.text.lower() options = [radio.text.lower() for radio in radios] - existing_answer = None for item in self.all_data: if self._sanitize_text(question_text) in item['question'] and item['type'] == 'radio': existing_answer = item - break - if existing_answer: - self._select_radio(radios, existing_answer['answer']) - return True - + self._select_radio(radios, existing_answer['answer']) + return True answer = self.gpt_answerer.answer_question_from_options(question_text, options) self._save_questions_to_json({'type': 'radio', 'question': question_text, 'answer': answer}) self._select_radio(radios, answer) @@ -283,12 +275,10 @@ class LinkedInEasyApplier: answer = self.gpt_answerer.answer_question_textual_wide_range(question_text) existing_answer = None for item in self.all_data: - if item['question'] == self._sanitize_text(question_text) and item['type'] == question_type: + if 'cover' not in item['question'] and item['question'] == self._sanitize_text(question_text) and item['type'] == question_type: existing_answer = item - break - if existing_answer: - self._enter_text(text_field, existing_answer['answer']) - return True + self._enter_text(text_field, existing_answer['answer']) + return True self._save_questions_to_json({'type': question_type, 'question': question_text, 'answer': answer}) self._enter_text(text_field, answer) return True @@ -302,15 +292,12 @@ class LinkedInEasyApplier: answer_date = self.gpt_answerer.answer_question_date() answer_text = answer_date.strftime("%Y-%m-%d") - existing_answer = None for item in self.all_data: if self._sanitize_text(question_text) in item['question'] and item['type'] == 'date': existing_answer = item - break - if existing_answer: - self._enter_text(date_field, existing_answer['answer']) - return True + self._enter_text(date_field, existing_answer['answer']) + return True self._save_questions_to_json({'type': 'date', 'question': question_text, 'answer': answer_text}) self._enter_text(date_field, answer_text) @@ -325,16 +312,12 @@ class LinkedInEasyApplier: if dropdown: select = Select(dropdown) options = [option.text for option in select.options] - existing_answer = None for item in self.all_data: if self._sanitize_text(question_text) in item['question'] and item['type'] == 'dropdown': existing_answer = item - break - if existing_answer: - self._select_dropdown_option(dropdown, existing_answer['answer']) - return True - + self._select_dropdown_option(dropdown, existing_answer['answer']) + return True answer = self.gpt_answerer.answer_question_from_options(question_text, options) self._save_questions_to_json({'type': 'dropdown', 'question': question_text, 'answer': answer}) self._select_dropdown_option(dropdown, answer) @@ -385,7 +368,6 @@ class LinkedInEasyApplier: tb_str = traceback.format_exc() raise Exception(f"Error saving questions data to JSON file: \nTraceback:\n{tb_str}") - def _sanitize_text(self, text: str) -> str: sanitized_text = text.lower() sanitized_text = sanitized_text.strip() diff --git a/src/linkedIn_job_manager.py b/src/linkedIn_job_manager.py index d368d71..7a87ae5 100644 --- a/src/linkedIn_job_manager.py +++ b/src/linkedIn_job_manager.py @@ -53,18 +53,6 @@ class LinkedInJobManager: def set_resume_generator_manager(self, resume_generator_manager): self.resume_generator_manager = resume_generator_manager - """ def old_question(self): - self.set_old_answers = {} - file_path = 'data_folder/output/old_Questions.csv' - if os.path.exists(file_path): - with open(file_path, 'r', newline='', encoding='utf-8', errors='ignore') as file: - csv_reader = csv.reader(file, delimiter=',', quotechar='"') - for row in csv_reader: - if len(row) == 3: - answer_type, question_text, answer = row - self.set_old_answers[(answer_type.lower(), question_text.lower())] = answer""" - - def start_applying(self): self.easy_applier_component = LinkedInEasyApplier(self.driver, self.resume_path, self.set_old_answers, self.gpt_answerer, self.resume_generator_manager) searches = list(product(self.positions, self.locations)) From c69fb6536b8449a99214e794f643eaa5ccfdbe6f Mon Sep 17 00:00:00 2001 From: Akshay Vinod <67356841+hakunama1ata@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:14:40 +0200 Subject: [PATCH 24/41] Added Encoding for Unicode --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 9685677..a17681e 100644 --- a/main.py +++ b/main.py @@ -162,7 +162,7 @@ def create_and_run_bot(email: str, password: str, parameters: dict, openai_api_k try: style_manager = StyleManager() resume_generator = ResumeGenerator() - with open(parameters['uploads']['plainTextResume'], "r") as file: + with open(parameters['uploads']['plainTextResume'], "r", encoding='utf-8') as file: plain_text_resume = file.read() resume_object = Resume(plain_text_resume) resume_generator_manager = FacadeManager(openai_api_key, style_manager, resume_generator, resume_object, Path("data_folder/output")) From f9b6f363573c5dc1036b26ff7a3a1ae50899b835 Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:07:44 +0530 Subject: [PATCH 25/41] Add bug issue template --- .github/CONTRIBUTING.md | 0 .github/ISSUE_TEMPLATE/bug-issue.yml | 85 ++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/ISSUE_TEMPLATE/bug-issue.yml diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml new file mode 100644 index 0000000..64dbd43 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -0,0 +1,85 @@ +name: Bug report +description: Report a bug or an issue that isn't working as expected. +title: "[BUG]: " +labels: ["bug"] +assignees: "" + +body: + - type: markdown + attributes: + value: | + Please fill out the following information to help us resolve the issue. + + - type: input + id: description + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is. + placeholder: "Describe the bug in detail..." + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: | + Steps to reproduce the behavior: + 1. Go to '...' + 2. Click on '...' + 3. Scroll down to '...' + 4. See error + placeholder: "List the steps to reproduce the bug..." + validations: + required: true + + - type: input + id: expected + attributes: + label: Expected behavior + description: What you expected to happen. + placeholder: "What was the expected result?" + validations: + required: true + + - type: input + id: actual + attributes: + label: Actual behavior + description: What actually happened instead. + placeholder: "What happened instead?" + validations: + required: true + + - type: dropdown + id: environment + attributes: + label: Environment + description: Specify the environment where the bug occurred. + options: + - label: Production + value: production + - label: Development + value: development + - label: Staging + value: staging + validations: + required: true + + - type: input + id: version + attributes: + label: Version + description: Version of the application where the bug occurred. + placeholder: "e.g., 1.0.0" + validations: + required: false + + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any other context about the problem here. + placeholder: "Any additional information..." + validations: + required: false From 9e88d3ba8d10014ae83e633dd04dda62e7abf200 Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:22:08 +0530 Subject: [PATCH 26/41] Add config for templates --- .github/ISSUE_TEMPLATE/config.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..9dc9d50 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Questions + url: t.me/AIhawkCommunity + about: You can join the discussions on Telegram. + - name: New issue + url: https://github.com/feder-cr/linkedIn_auto_jobs_applier_with_AI/blob/v3/.github/CONTRIBUTING.md + about: Before opening a new issue, please make sure to read CONTRIBUTING.md From 14fff0b05110800c0fd88e4b5c8b147ac76a756b Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:25:31 +0530 Subject: [PATCH 27/41] prettified yml --- .github/ISSUE_TEMPLATE/bug-issue.yml | 21 +++++++-------------- .github/ISSUE_TEMPLATE/config.yml | 5 +++-- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index 64dbd43..1bbd001 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -1,24 +1,22 @@ name: Bug report description: Report a bug or an issue that isn't working as expected. title: "[BUG]: " -labels: ["bug"] +labels: + - bug assignees: "" - body: - type: markdown attributes: value: | Please fill out the following information to help us resolve the issue. - - type: input id: description attributes: label: Describe the bug description: A clear and concise description of what the bug is. - placeholder: "Describe the bug in detail..." + placeholder: Describe the bug in detail... validations: required: true - - type: textarea id: steps attributes: @@ -29,28 +27,25 @@ body: 2. Click on '...' 3. Scroll down to '...' 4. See error - placeholder: "List the steps to reproduce the bug..." + placeholder: List the steps to reproduce the bug... validations: required: true - - type: input id: expected attributes: label: Expected behavior description: What you expected to happen. - placeholder: "What was the expected result?" + placeholder: What was the expected result? validations: required: true - - type: input id: actual attributes: label: Actual behavior description: What actually happened instead. - placeholder: "What happened instead?" + placeholder: What happened instead? validations: required: true - - type: dropdown id: environment attributes: @@ -65,7 +60,6 @@ body: value: staging validations: required: true - - type: input id: version attributes: @@ -74,12 +68,11 @@ body: placeholder: "e.g., 1.0.0" validations: required: false - - type: textarea id: additional attributes: label: Additional context description: Add any other context about the problem here. - placeholder: "Any additional information..." + placeholder: Any additional information... validations: required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9dc9d50..bc3f586 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -4,5 +4,6 @@ contact_links: url: t.me/AIhawkCommunity about: You can join the discussions on Telegram. - name: New issue - url: https://github.com/feder-cr/linkedIn_auto_jobs_applier_with_AI/blob/v3/.github/CONTRIBUTING.md - about: Before opening a new issue, please make sure to read CONTRIBUTING.md + url: >- + https://github.com/feder-cr/linkedIn_auto_jobs_applier_with_AI/blob/v3/.github/CONTRIBUTING.md + about: "Before opening a new issue, please make sure to read CONTRIBUTING.md" From 059f5e93025a9afaea1897d3f44c15ec77dc1bc8 Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:28:26 +0530 Subject: [PATCH 28/41] fixed erros for arrays --- .github/ISSUE_TEMPLATE/bug-issue.yml | 37 +++++++++++----------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index 1bbd001..6fc75a5 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -1,22 +1,22 @@ name: Bug report description: Report a bug or an issue that isn't working as expected. title: "[BUG]: " -labels: - - bug -assignees: "" +labels: ["bug"] +assignees: [] + body: - type: markdown attributes: value: | Please fill out the following information to help us resolve the issue. + - type: input id: description attributes: label: Describe the bug description: A clear and concise description of what the bug is. - placeholder: Describe the bug in detail... - validations: - required: true + placeholder: "Describe the bug in detail..." + - type: textarea id: steps attributes: @@ -27,25 +27,22 @@ body: 2. Click on '...' 3. Scroll down to '...' 4. See error - placeholder: List the steps to reproduce the bug... - validations: - required: true + placeholder: "List the steps to reproduce the bug..." + - type: input id: expected attributes: label: Expected behavior description: What you expected to happen. - placeholder: What was the expected result? - validations: - required: true + placeholder: "What was the expected result?" + - type: input id: actual attributes: label: Actual behavior description: What actually happened instead. - placeholder: What happened instead? - validations: - required: true + placeholder: "What happened instead?" + - type: dropdown id: environment attributes: @@ -58,21 +55,17 @@ body: value: development - label: Staging value: staging - validations: - required: true + - type: input id: version attributes: label: Version description: Version of the application where the bug occurred. placeholder: "e.g., 1.0.0" - validations: - required: false + - type: textarea id: additional attributes: label: Additional context description: Add any other context about the problem here. - placeholder: Any additional information... - validations: - required: false + placeholder: "Any additional information..." From 08810adf054e57535afb2f1a78a1f0836a4ffade Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:38:23 +0530 Subject: [PATCH 29/41] fixed yaml indentations --- .github/ISSUE_TEMPLATE/bug-issue.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index 6fc75a5..a6ff696 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -49,12 +49,9 @@ body: label: Environment description: Specify the environment where the bug occurred. options: - - label: Production - value: production - - label: Development - value: development - - label: Staging - value: staging + - Production + - Development + - Staging - type: input id: version From cee47f7d70e98a7380dd4957657607d4e76dfad2 Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:42:43 +0530 Subject: [PATCH 30/41] enhancement template --- .github/ISSUE_TEMPLATE/enhancement-issue.yml | 46 ++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/enhancement-issue.yml diff --git a/.github/ISSUE_TEMPLATE/enhancement-issue.yml b/.github/ISSUE_TEMPLATE/enhancement-issue.yml new file mode 100644 index 0000000..433ef84 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement-issue.yml @@ -0,0 +1,46 @@ +name: Feature request +description: Suggest a new feature or improvement for the project. +title: "[FEATURE]: " +labels: ["enhancement"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Thank you for suggesting a feature! Please fill out the form below to help us understand your idea. + + - type: input + id: summary + attributes: + label: Feature summary + description: Provide a short summary of the feature you're requesting. + placeholder: "Summarize the feature in a few words..." + + - type: textarea + id: description + attributes: + label: Feature description + description: A detailed description of the feature or improvement. + placeholder: "Describe the feature in detail..." + + - type: input + id: motivation + attributes: + label: Motivation + description: Explain why this feature would be beneficial and how it solves a problem. + placeholder: "Why do you need this feature?" + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: List any alternative solutions or features you've considered. + placeholder: "Are there any alternative features or solutions you’ve considered?" + + - type: input + id: additional + attributes: + label: Additional context + description: Add any other context or screenshots to support your feature request. + placeholder: "Any additional information..." From 512efda803d0c358c48f33e6cff8cb2e7cb6b7c2 Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:44:59 +0530 Subject: [PATCH 31/41] invalid issue template --- .github/ISSUE_TEMPLATE/invalid-issue.yml | 39 ++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/invalid-issue.yml diff --git a/.github/ISSUE_TEMPLATE/invalid-issue.yml b/.github/ISSUE_TEMPLATE/invalid-issue.yml new file mode 100644 index 0000000..cc4f27f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/invalid-issue.yml @@ -0,0 +1,39 @@ +name: Invalid issue report +description: Report an issue that doesn't seem correct or is invalid. +title: "[INVALID]: " +labels: ["invalid"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + If you've identified an issue that seems incorrect or should not exist, please fill out the form below to provide more details. + + - type: input + id: reason + attributes: + label: Reason for invalidation + description: Briefly explain why this issue is considered invalid or incorrect. + placeholder: "Why do you think this issue is invalid?" + + - type: textarea + id: steps + attributes: + label: Steps to validate + description: Provide steps or evidence that confirm the issue is invalid. + placeholder: "Explain how you verified this issue is not valid..." + + - type: input + id: original_issue + attributes: + label: Related issue (if applicable) + description: Provide a link to the original issue if this is related to an existing one. + placeholder: "Link to the related issue (if applicable)" + + - type: input + id: additional + attributes: + label: Additional context + description: Any additional information you think is necessary. + placeholder: "Add any other context here..." From db69b0f1bfc97f0e8fc628a69cc3cd30cbaa8707 Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:46:44 +0530 Subject: [PATCH 32/41] documentation issue template --- .../ISSUE_TEMPLATE/documentation-issue.yml | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/documentation-issue.yml diff --git a/.github/ISSUE_TEMPLATE/documentation-issue.yml b/.github/ISSUE_TEMPLATE/documentation-issue.yml new file mode 100644 index 0000000..14f63a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation-issue.yml @@ -0,0 +1,39 @@ +name: Documentation request +description: Suggest improvements or additions to the project's documentation. +title: "[DOCS]: " +labels: ["documentation"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Thanks for helping to improve the project's documentation! Please provide the following details to ensure your request is clear. + + - type: input + id: doc_section + attributes: + label: Affected documentation section + description: Specify which part of the documentation needs improvement or addition. + placeholder: "e.g., Installation Guide, API Reference..." + + - type: textarea + id: description + attributes: + label: Documentation improvement description + description: Describe the specific improvements or additions you suggest. + placeholder: "Explain what changes you propose and why..." + + - type: input + id: reason + attributes: + label: Why is this change necessary? + description: Explain why the documentation needs to be updated or expanded. + placeholder: "Describe the issue or gap in the documentation..." + + - type: input + id: additional + attributes: + label: Additional context + description: Add any other context, such as related documentation, external resources, or screenshots. + placeholder: "Add any other supporting information..." From 6ef07a5f30f9b6caec830ef57efb2910c28798ce Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:49:10 +0530 Subject: [PATCH 33/41] questions and duplicates template added --- .github/ISSUE_TEMPLATE/duplicate-issue.yml | 32 ++++++++++++++++++ .github/ISSUE_TEMPLATE/question-issue.yml | 39 ++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/duplicate-issue.yml create mode 100644 .github/ISSUE_TEMPLATE/question-issue.yml diff --git a/.github/ISSUE_TEMPLATE/duplicate-issue.yml b/.github/ISSUE_TEMPLATE/duplicate-issue.yml new file mode 100644 index 0000000..8057a32 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/duplicate-issue.yml @@ -0,0 +1,32 @@ +name: Duplicate issue report +description: Report an issue or pull request that already exists in the project. +title: "[DUPLICATE]: " +labels: ["duplicate"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Please provide information about the duplicate issue or pull request. + + - type: input + id: duplicate_link + attributes: + label: Link to the original issue/pull request + description: Provide the URL of the original issue or pull request that duplicates this one. + placeholder: "https://github.com/your-repo/issue/123" + + - type: input + id: reason + attributes: + label: Reason for marking as duplicate + description: Explain why this issue is considered a duplicate. + placeholder: "Briefly explain why this is a duplicate." + + - type: input + id: additional + attributes: + label: Additional context + description: Add any additional context or supporting information. + placeholder: "Any additional information or comments..." diff --git a/.github/ISSUE_TEMPLATE/question-issue.yml b/.github/ISSUE_TEMPLATE/question-issue.yml new file mode 100644 index 0000000..e2e949e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question-issue.yml @@ -0,0 +1,39 @@ +name: Question or Information Request +description: Ask a question or request more information related to the project. +title: "[QUESTION]: " +labels: ["question"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Please fill out the form below to ask your question or request further information. + + - type: input + id: question_summary + attributes: + label: Summary of your question + description: Provide a brief summary of your question or information request. + placeholder: "Summarize your question in a few words..." + + - type: textarea + id: question_details + attributes: + label: Question details + description: Provide a detailed explanation of your question or what information you're requesting. + placeholder: "Describe your question or information request in detail..." + + - type: input + id: context + attributes: + label: Context for the question + description: Provide any relevant context or background information that may help clarify your question. + placeholder: "Add context for your question (e.g., where you encountered the issue, what you're trying to do)..." + + - type: input + id: additional + attributes: + label: Additional context + description: Add any additional information that may help answer your question. + placeholder: "Any extra information or comments..." From c77d891a769d8b98064ef76a881013eb0e2870e2 Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:52:42 +0530 Subject: [PATCH 34/41] wont fix and help templates added --- .github/ISSUE_TEMPLATE/help-issue.yml | 39 ++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/wontfix-issue.yml | 32 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/help-issue.yml create mode 100644 .github/ISSUE_TEMPLATE/wontfix-issue.yml diff --git a/.github/ISSUE_TEMPLATE/help-issue.yml b/.github/ISSUE_TEMPLATE/help-issue.yml new file mode 100644 index 0000000..4177fcd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/help-issue.yml @@ -0,0 +1,39 @@ +name: Help wanted +description: Request additional help or attention for an issue that needs extra effort. +title: "[HELP WANTED]: " +labels: ["help wanted"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + We need additional help with this issue. Please provide as much detail as possible to assist contributors. + + - type: textarea + id: issue_description + attributes: + label: Issue description + description: Provide a detailed description of the issue and what kind of help is needed. + placeholder: "Describe the issue and the type of help required..." + + - type: input + id: specific_tasks + attributes: + label: Specific tasks + description: List any specific tasks or sub-tasks where help is needed. + placeholder: "List specific tasks or areas where help is needed..." + + - type: input + id: additional_resources + attributes: + label: Additional resources + description: Provide links to related documentation, resources, or references that might help contributors. + placeholder: "Link to relevant resources or documentation..." + + - type: input + id: additional + attributes: + label: Additional context + description: Add any extra information or context that might help in addressing the issue. + placeholder: "Any additional information or comments..." diff --git a/.github/ISSUE_TEMPLATE/wontfix-issue.yml b/.github/ISSUE_TEMPLATE/wontfix-issue.yml new file mode 100644 index 0000000..77d5871 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/wontfix-issue.yml @@ -0,0 +1,32 @@ +name: Won't fix +description: Mark an issue as won't fix if it will not be addressed or resolved. +title: "[WONTFIX]: " +labels: ["wontfix"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + This issue will not be fixed. Please provide reasons or context for why the issue is being closed as won't fix. + + - type: textarea + id: reason + attributes: + label: Reason for won't fix + description: Explain why this issue will not be fixed or addressed. + placeholder: "Describe the reason why this issue is being marked as won't fix..." + + - type: input + id: decision_maker + attributes: + label: Decision maker + description: Specify who made the decision to mark the issue as won't fix. + placeholder: "Name of the person or team responsible for this decision..." + + - type: input + id: additional + attributes: + label: Additional context + description: Add any other context or information relevant to the decision. + placeholder: "Any additional information or comments..." From b780bae4a0e712e147c2f50776c49e414794f5d1 Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 06:55:35 +0530 Subject: [PATCH 35/41] goodfirst template added --- .github/ISSUE_TEMPLATE/goodfirst-issue.yml | 46 ++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/goodfirst-issue.yml diff --git a/.github/ISSUE_TEMPLATE/goodfirst-issue.yml b/.github/ISSUE_TEMPLATE/goodfirst-issue.yml new file mode 100644 index 0000000..212a0d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/goodfirst-issue.yml @@ -0,0 +1,46 @@ +name: Good first issue +description: Suitable for newcomers or those new to the project. +title: "[GOOD FIRST ISSUE]: " +labels: ["good first issue"] +assignees: [] + +body: + - type: markdown + attributes: + value: | + Welcome to contributing to our project! This issue is marked as a "Good First Issue," which means it is a great starting point for new contributors. Please provide the following information to help us understand your issue. + + - type: input + id: issue_summary + attributes: + label: Issue summary + description: Provide a brief summary of the issue or task. + placeholder: "Summarize the issue or task..." + + - type: textarea + id: detailed_description + attributes: + label: Detailed description + description: Provide a detailed description of what needs to be done, including any relevant background information or steps. + placeholder: "Describe the issue or task in detail, including any relevant information..." + + - type: input + id: steps_to_reproduce + attributes: + label: Steps to reproduce (if applicable) + description: If this issue involves a bug, list the steps to reproduce the problem. + placeholder: "List the steps to reproduce the issue (if applicable)..." + + - type: input + id: expected_outcome + attributes: + label: Expected outcome + description: Describe what you expect to happen once the issue is resolved. + placeholder: "Describe the expected outcome..." + + - type: input + id: additional_context + attributes: + label: Additional context + description: Add any other context or information that might be helpful for resolving the issue. + placeholder: "Any additional information or comments..." From 5069a0b18c3f9693d8c33a357d70add0117f8e29 Mon Sep 17 00:00:00 2001 From: Syed Date: Wed, 4 Sep 2024 07:20:38 +0530 Subject: [PATCH 36/41] added contributing.md for issues --- .github/CONTRIBUTING.md | 65 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e69de29..04e478a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,65 @@ +# Issues Reporting Guidelines + +Welcome to the LinkedIn Auto Jobs Applier with AI issues page! To keep things organized and ensure issues are resolved quickly, please follow the guidelines below when submitting a bug report, feature request, or any other issue. + +## Before You Submit an Issue + +### 1. Search Existing Issues + +Please search through the existing open issues and closed issues to ensure your issue hasn’t already been reported. This helps avoid duplicates and allows us to focus on unresolved problems. + +### 2. Check Documentation + +Review the README and any available documentation to see if your issue is covered. + +### 3. Provide Detailed Information + +If you are reporting a bug, make sure you include enough details to reproduce the issue. The more information you provide, the faster we can diagnose and fix the problem. + +## Issue Types + +### 1. Bug Reports + +Please include the following information: + +- **Description:** A clear and concise description of the problem. +- **Steps to Reproduce:** Provide detailed steps to reproduce the bug. +- **Expected Behavior:** What should have happened. +- **Actual Behavior:** What actually happened. +- **Environment Details:** Include your OS, browser version (if applicable), and any other relevant environment details. +- **Logs/Screenshots:** If applicable, attach screenshots or log outputs. + +### 2. Feature Requests + +For new features or improvements: + +- Clearly describe the feature you would like to see. +- Explain the problem this feature would solve or the benefit it would bring. +- If possible, provide examples or references to similar features in other tools or platforms. + +### 3. Questions/Discussions + +- If you’re unsure whether something is a bug or if you’re seeking clarification on functionality, you can ask a question. Please make sure to label your issue as a question. + +## Issue Labeling and Response Time + +We use the following labels to categorize issues: + +- **bug:** An issue where something isn't functioning as expected. +- **documentation:** Improvements or additions to project documentation. +- **duplicate:** This issue or pull request already exists elsewhere. +- **enhancement:** A request for a new feature or improvement. +- **good first issue:** A simple issue suitable for newcomers. +- **help wanted:** The issue needs extra attention or assistance. +- **invalid:** The issue is not valid or doesn't seem correct. +- **question:** Additional information or clarification is needed. +- **wontfix:** The issue will not be fixed or addressed. +- We aim to respond to issues as early as possible. Please be patient, as maintainers may have limited availability. + +## Contributing Fixes + +If you’re able to contribute a fix for an issue: + +1. Fork the repository and create a new branch for your fix. +2. Reference the issue number in your branch and pull request. +3. Submit a pull request with a detailed description of the changes and how they resolve the issue. From 75468db0b937728704faf777b890978074ed4929 Mon Sep 17 00:00:00 2001 From: Vinicius Tavares Date: Wed, 4 Sep 2024 10:30:18 -0300 Subject: [PATCH 37/41] add config to apply once at company --- data_folder/config.yaml | 2 ++ data_folder_example/config.yaml | 3 ++- src/linkedIn_job_manager.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/data_folder/config.yaml b/data_folder/config.yaml index 58a6f1c..22d4c1f 100644 --- a/data_folder/config.yaml +++ b/data_folder/config.yaml @@ -31,6 +31,8 @@ locations: - Country1 - Country2 +applyOnceAtCompany: [true/false] + distance: 100 companyBlacklist: diff --git a/data_folder_example/config.yaml b/data_folder_example/config.yaml index 6e362be..f0f6557 100644 --- a/data_folder_example/config.yaml +++ b/data_folder_example/config.yaml @@ -26,10 +26,11 @@ date: positions: - Software Tester - locations: - USA +applyOnceAtCompany: [true/false] + distance: 100 companyBlacklist: diff --git a/src/linkedIn_job_manager.py b/src/linkedIn_job_manager.py index 7a87ae5..e42c087 100644 --- a/src/linkedIn_job_manager.py +++ b/src/linkedIn_job_manager.py @@ -36,6 +36,7 @@ class LinkedInJobManager: self.title_blacklist = parameters.get('titleBlacklist', []) or [] self.positions = parameters.get('positions', []) self.locations = parameters.get('locations', []) + self.apply_once_at_company = parameters.get('applyOnceAtCompany', False) self.base_search_url = self.get_base_search_url(parameters) self.seen_jobs = [] resume_path = parameters.get('uploads', {}).get('resume', None) @@ -121,6 +122,12 @@ class LinkedInJobManager: utils.printyellow(f"Blacklisted {job.title} at {job.company}, skipping...") self.write_to_file(job, "skipped") continue + if self.is_already_applied_to_job(job.title, job.company, job.link): + self.write_to_file(job, "skipped") + continue + if self.is_already_applied_to_company(job.company): + self.write_to_file(job, "skipped") + continue try: if job.apply_method not in {"Continue", "Applied", "Apply"}: self.easy_applier_component.job_apply(job) @@ -206,3 +213,28 @@ class LinkedInJobManager: company_blacklisted = company.strip().lower() in (word.strip().lower() for word in self.company_blacklist) link_seen = link in self.seen_jobs return title_blacklisted or company_blacklisted or link_seen + + def is_already_applied_to_job(self, job_title, company, link): + link_seen = link in self.seen_jobs + if link_seen: + utils.printyellow(f"Already applied to job: {job_title} at {company}, skipping...") + return link_seen + + def is_already_applied_to_company(self, company): + if not self.apply_once_at_company: + return False + + output_files = ["success.json"] + for file_name in output_files: + file_path = self.output_file_directory / file_name + if file_path.exists(): + with open(file_path, 'r', encoding='utf-8') as f: + try: + existing_data = json.load(f) + for applied_job in existing_data: + if applied_job['company'].strip().lower() == company.strip().lower(): + utils.printyellow(f"Already applied at {company} (once per company policy), skipping...") + return True + except json.JSONDecodeError: + continue + return False \ No newline at end of file From 89a8c462f63383886be4247a55955fa75d1e8d77 Mon Sep 17 00:00:00 2001 From: Vinicius Nunes <104783995+viniciusnunest@users.noreply.github.com> Date: Wed, 4 Sep 2024 12:51:52 -0300 Subject: [PATCH 38/41] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 15fe7b8..3994b63 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,8 @@ This file defines your job search parameters and bot behavior. Each section cont - Italy - London ``` +- `applyOnceAtCompany: [True/False]` + - Set if you will apply in more than one opportunity per company - `distance: [number]` - Set the radius for your job search in miles From d7fd4009183e30c0db9b33c1f11b346e81662063 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Wed, 4 Sep 2024 19:51:08 +0200 Subject: [PATCH 39/41] Added a little list to save jobs_id to avoid re-iterate applied already jobs --- src/linkedin-api.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/linkedin-api.py b/src/linkedin-api.py index ae37845..70088b8 100644 --- a/src/linkedin-api.py +++ b/src/linkedin-api.py @@ -9,6 +9,8 @@ import json logging.basicConfig(level=logging.INFO) class LinkedInEvolvedAPI(Linkedin): + already_applied_jobs: List[str] = [] + def __init__(self, username, password): super().__init__(username, password) @@ -351,6 +353,8 @@ class LinkedInEvolvedAPI(Linkedin): # Push the commit to the repository and create a pull request to the v3 branch. + def set_job_as_applied(self, job_id: str) -> None: + self.already_applied_jobs.append(job_id) @@ -360,10 +364,13 @@ class LinkedInEvolvedAPI(Linkedin): ## EXAMPLE USAGE if __name__ == "__main__": - api: LinkedInEvolvedAPI = LinkedInEvolvedAPI(username="", password="") + api: LinkedInEvolvedAPI = LinkedInEvolvedAPI(username="", password="") jobs = api.search_jobs(keywords="Frontend Developer", location_name="Italia", limit=5, easy_apply=True, offset=1) for job in jobs: job_id: str = job["job_id"] + if job_id in api.already_applied_jobs: + logging.info(f"Already applied to job {job_id}, skipping it") + continue fields = api.get_fields_for_easy_apply(job_id) for field in fields: From 5673308a3d8d917484ae4b03b8314b0875493d37 Mon Sep 17 00:00:00 2001 From: karnoark Date: Thu, 5 Sep 2024 16:38:23 +0530 Subject: [PATCH 40/41] stripped away additional text --- src/gpt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gpt.py b/src/gpt.py index 22c7d6c..77a9992 100644 --- a/src/gpt.py +++ b/src/gpt.py @@ -326,7 +326,11 @@ class GPTAnswerer: prompt = ChatPromptTemplate.from_template(section_prompt) chain = prompt | self.llm_cheap | StrOutputParser() output = chain.invoke({"question": question}) - section_name = output.lower().replace(" ", "_") + match = re.search(r"(Personal information|Self Identification|Legal Authorization|Work Preferences|Education Details|Experience Details|Projects|Availability|Salary Expectations|Certifications|Languages|Interests|Cover letter)", output, re.IGNORECASE) + if not match: + raise ValueError("Could not extract section name from the response.") + + section_name = match.group(1).lower().replace(" ", "_") if section_name == "cover_letter": chain = chains.get(section_name) output = chain.invoke({"resume": self.resume, "job_description": self.job_description}) From 104f99aa96cc822a5524d4d411de93f2d90ef14e Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Thu, 5 Sep 2024 17:27:04 +0200 Subject: [PATCH 41/41] Fixed search filter to avoid fixed results --- src/linkedin-api.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/linkedin-api.py b/src/linkedin-api.py index 70088b8..37f727d 100644 --- a/src/linkedin-api.py +++ b/src/linkedin-api.py @@ -47,7 +47,7 @@ class LinkedInEvolvedAPI(Linkedin): industries: Optional[List[str]] = None, location_name: Optional[str] = None, remote: Optional[List[Union[Literal["1"], Literal["2"], Literal["3"]]]] = None, - listed_at=24 * 60 * 60, + listed_at: None | int = None, distance: Optional[int] = None, easy_apply: Optional[bool] = True, limit=-1, @@ -72,8 +72,8 @@ class LinkedInEvolvedAPI(Linkedin): :type location_name: str, optional :param remote: Filter for remote jobs, onsite or hybrid. onsite:"1", remote:"2", hybrid:"3" :type remote: list, optional - :param listed_at: maximum number of seconds passed since job posting. 86400 will filter job postings posted in last 24 hours. - :type listed_at: int/str, optional. Default value is equal to 24 hours. + :param listed_at: maximum number of seconds passed since job posting. 86400 will filter job postings posted in last 24 hours, default is None + :type listed_at: int or none, if none, no filter applied, otherwise, filter applied in seconds :param distance: maximum distance from location in miles :type distance: int/str, optional. If not specified, None or 0, the default value of 25 miles applied. :param easy_apply: filter for jobs that are easy to apply to @@ -115,7 +115,8 @@ class LinkedInEvolvedAPI(Linkedin): if easy_apply: query["selectedFilters"]["applyWithLinkedin"] = "List(true)" - query["selectedFilters"]["timePostedRange"] = f"List(r{listed_at})" + if listed_at: + query["selectedFilters"]["timePostedRange"] = f"List(r{listed_at})" query["spellCorrectionEnabled"] = "true" query_string = ( @@ -144,7 +145,6 @@ class LinkedInEvolvedAPI(Linkedin): headers={"accept": "application/vnd.linkedin.normalized+json+2.1"}, ) data = res.json() - elements = data.get("included", []) new_data = [] for e in elements: @@ -365,9 +365,12 @@ class LinkedInEvolvedAPI(Linkedin): ## EXAMPLE USAGE if __name__ == "__main__": api: LinkedInEvolvedAPI = LinkedInEvolvedAPI(username="", password="") - jobs = api.search_jobs(keywords="Frontend Developer", location_name="Italia", limit=5, easy_apply=True, offset=1) + jobs = api.search_jobs(keywords="Frontend Developer", location_name="Italia", limit=100, easy_apply=True, offset=1, listed_at=None) for job in jobs: job_id: str = job["job_id"] + print(f"Job ID: {job_id}") + continue + if job_id in api.already_applied_jobs: logging.info(f"Already applied to job {job_id}, skipping it") continue