Compare commits

...

56 Commits
main ... v3

Author SHA1 Message Date
Federico
ffca48f6c3
Merge pull request #274 from feder-cr/main
v3
2024-09-04 13:22:24 +02:00
Federico
fff143d407
Merge pull request #273 from activatedbonkers/v3
Added templates for issues, and guidelines
2024-09-04 13:21:32 +02:00
Federico
4fffb64a81
Merge pull request #223 from anton6tak/open-ai-tier-upgrade-runtime-errors
add information about request limit errors and account type issues
2024-09-04 13:21:04 +02:00
Syed
5069a0b18c added contributing.md for issues 2024-09-04 07:20:38 +05:30
Syed
b780bae4a0 goodfirst template added 2024-09-04 06:55:35 +05:30
Syed
c77d891a76 wont fix and help templates added 2024-09-04 06:52:42 +05:30
Syed
6ef07a5f30 questions and duplicates template added 2024-09-04 06:49:10 +05:30
Syed
db69b0f1bf documentation issue template 2024-09-04 06:46:44 +05:30
Syed
512efda803 invalid issue template 2024-09-04 06:44:59 +05:30
Syed
cee47f7d70 enhancement template 2024-09-04 06:42:43 +05:30
Syed
08810adf05 fixed yaml indentations 2024-09-04 06:38:23 +05:30
Syed
059f5e9302 fixed erros for arrays 2024-09-04 06:28:26 +05:30
Syed
14fff0b051 prettified yml 2024-09-04 06:25:31 +05:30
Syed
9e88d3ba8d Add config for templates 2024-09-04 06:22:08 +05:30
Syed
f9b6f36357 Add bug issue template 2024-09-04 06:07:44 +05:30
Federico
e3bcc9d5e8
Merge pull request #266 from feder-cr/main
add
2024-09-03 19:08:44 +02:00
Federico
314f054fc0
Merge pull request #204 from mmcc007/main
plain_text_resume.yaml generator from text resume
2024-09-03 17:54:20 +02:00
Federico
1b6c388a0f
Merge pull request #247 from spectreDeveloper/v3
Instructions to apply_jobs function toDo
2024-09-03 17:53:56 +02:00
Federico
a17d3766d4
Merge pull request #260 from feder-cr/main
cover letter fixed
2024-09-03 17:14:38 +02:00
Manu Altieri
092f4e7032 Added instructions toDo; about apply method 2024-09-03 00:27:54 +02:00
Maurice McCabe
3511dbc88e
Merge branch 'v3' into main 2024-09-02 08:13:09 -07:00
Federico
6f383632a0
Merge pull request #230 from spectreDeveloper/v3
Added method to get fields required for fast apply via API, fixed easyApply filter for search.
2024-09-02 14:41:23 +02:00
Federico
15bbd61177
Merge pull request #199 from zaverichintan/v3
Added support for Ollama running locally or publicly hosted api
2024-09-02 14:27:48 +02:00
Federico
020d7a24dc
Merge branch 'v3' into v3 2024-09-02 14:27:29 +02:00
Manu Altieri
d2372523fa fixed the parsing of the required fields for apply 2024-09-02 11:10:18 +02:00
Maurice McCabe
685da0f9fc replace lib PYPDF2 with pdfminer.six 2024-09-02 01:44:54 -07:00
Maurice McCabe
23567ee7c4 updated for generating resume yaml from pdf 2024-09-02 01:25:38 -07:00
Maurice McCabe
ff087129b0 add support for converting a pdf to txt 2024-09-02 01:06:11 -07:00
Manu Altieri
d76a35fa7c 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 2024-09-01 22:53:05 +02:00
Manu Altieri
c0f186ba84 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 2024-09-01 22:48:01 +02:00
user
d1d9e9f6a3 config yaml file with default openai gpt freely hosted 2024-09-01 21:16:02 +02:00
user
28b8fa3746 README updated 2024-09-01 21:15:03 +02:00
anton6tak
bb4e0ed8a6 add information about request limit errors and account type issues 2024-09-01 10:49:36 -07:00
user
777d1fa4a6 Claude support with langchain antrhopic 2024-09-01 19:33:42 +02:00
Federico
60b3c9950d
Merge pull request #217 from earl562/main
Documentation TODO: https://github.com/feder-cr/linkedIn_auto_jobs_applier_with_AI/issues/193
2024-09-01 16:08:47 +02:00
EP
28e4f29e21
Merge branch 'v3' into main 2024-09-01 09:19:24 -04:00
Earl Perry
9ed0c3ea63 Instead of adding a documentation section, improved trouble shooting guide, with current issues, solutions, and addtional resources 2024-09-01 09:01:52 -04:00
user
6b4ebe1bd0 config.yaml corrected 2024-09-01 12:01:39 +02:00
user
a9d9e13474 Adapter class added 2024-09-01 11:59:43 +02:00
Maurice McCabe
aa5f319494
Merge branch 'v3' into main 2024-08-31 21:55:01 -07:00
Maurice McCabe
369c23791d use gpt-4o-mini 2024-08-31 15:58:55 -07:00
Maurice McCabe
67543499ab update cmdline utility to match README 2024-08-31 15:41:33 -07:00
Maurice McCabe
93691913f9 update cmdline utility to match README 2024-08-31 15:41:08 -07:00
Maurice McCabe
fbb04b5f67
Merge branch 'feder-cr:main' into main 2024-08-31 14:57:15 -07:00
Maurice McCabe
be8ddb8241 resume generator cmdline utility 2024-08-31 14:56:37 -07:00
user
ca4f56833a Added support for ollama endpoint and defining the LLM model in the config 2024-08-31 22:58:40 +02:00
Federico
3c0fbb4969
Merge pull request #200 from spectreDeveloper/v3
Added a new pyhon class linkedin-api.py, edited to allow to search only jobs with easyApply easly.
2024-08-31 21:07:56 +02:00
user
d964f599be Added support for Ollama running locally or publicly hosted api 2024-08-31 20:19:33 +02:00
Manu Altieri
bac05ad04c Adding linkedin-api.py to search jobs without use selenium (requirements) 2024-08-31 19:39:44 +02:00
Manu Altieri
9fc3274b9a Adding linkedin-api.py to search jobs without use selenium. 2024-08-31 19:31:31 +02:00
Federico
6f3b3252ba
Merge pull request #186 from air55555/v3
Readme upd to ease troubleshooting and fixing
2024-08-31 14:11:55 +02:00
1
761bb91e96 Readme upd to ease troubleshooting and fixing 2024-08-31 12:41:05 +03:00
feder-cr
0e90d4be7d now we use pydantic for yaml validator 2024-08-29 19:01:42 +02:00
Federico
45bc6ea95d
Merge pull request #135 from spectreDeveloper/v3
logged in functions optimized
2024-08-29 17:31:56 +02:00
feder-cr
e898df9880 v3 lib 2024-08-29 17:11:38 +02:00
Manu Altieri
f948443a39 Make the function better, because is not needed check selectors. when a user is logged on linkedin will be redirected already on /feed/ endpoint, otherwise url will remain https://linkedin.com, so is possible check the state of login just using this approach 2024-08-29 17:08:26 +02:00
24 changed files with 1372 additions and 45 deletions

65
.github/CONTRIBUTING.md vendored Normal file
View File

@ -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 hasnt 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 youre unsure whether something is a bug or if youre 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 youre 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.

68
.github/ISSUE_TEMPLATE/bug-issue.yml vendored Normal file
View File

@ -0,0 +1,68 @@
name: Bug report
description: Report a bug or an issue that isn't working as expected.
title: "[BUG]: <Provide a clear, descriptive title>"
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..."
- 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..."
- type: input
id: expected
attributes:
label: Expected behavior
description: What you expected to happen.
placeholder: "What was the expected result?"
- type: input
id: actual
attributes:
label: Actual behavior
description: What actually happened instead.
placeholder: "What happened instead?"
- type: dropdown
id: environment
attributes:
label: Environment
description: Specify the environment where the bug occurred.
options:
- Production
- Development
- Staging
- type: input
id: version
attributes:
label: Version
description: Version of the application where the bug occurred.
placeholder: "e.g., 1.0.0"
- type: textarea
id: additional
attributes:
label: Additional context
description: Add any other context about the problem here.
placeholder: "Any additional information..."

9
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,9 @@
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"

View File

@ -0,0 +1,39 @@
name: Documentation request
description: Suggest improvements or additions to the project's documentation.
title: "[DOCS]: <Provide a short title>"
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..."

View File

@ -0,0 +1,32 @@
name: Duplicate issue report
description: Report an issue or pull request that already exists in the project.
title: "[DUPLICATE]: <Provide a brief title>"
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..."

View File

@ -0,0 +1,46 @@
name: Feature request
description: Suggest a new feature or improvement for the project.
title: "[FEATURE]: <Provide a descriptive title>"
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 youve 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..."

View File

@ -0,0 +1,46 @@
name: Good first issue
description: Suitable for newcomers or those new to the project.
title: "[GOOD FIRST ISSUE]: <Provide a descriptive title>"
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..."

39
.github/ISSUE_TEMPLATE/help-issue.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Help wanted
description: Request additional help or attention for an issue that needs extra effort.
title: "[HELP WANTED]: <Provide a descriptive title>"
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..."

View File

@ -0,0 +1,39 @@
name: Invalid issue report
description: Report an issue that doesn't seem correct or is invalid.
title: "[INVALID]: <Provide a brief title>"
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..."

View File

@ -0,0 +1,39 @@
name: Question or Information Request
description: Ask a question or request more information related to the project.
title: "[QUESTION]: <Provide a short title>"
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..."

View File

@ -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]: <Provide a brief title>"
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..."

126
README.md
View File

@ -148,11 +148,15 @@ 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).
- 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 <org> 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).
### 2. config.yaml
@ -211,6 +215,21 @@ This file defines your job search parameters and bot behavior. Each section cont
- Sales
- Marketing
```
#### 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:
- openai: gpt-4o
- ollama: llama2, mistral:v0.3
- 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
@ -452,6 +471,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 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 pdf-based or text-based resume. Follow these steps:
1. Prepare your resume in a pdf (.pdf file) or plain text (.txt file) format.
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.[pdf|txt] --output data_folder/plain_text_resume.yaml
```
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.
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
@ -501,18 +541,82 @@ 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/)
- **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.

132
assets/resume_schema.yaml Normal file
View File

@ -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]

View File

@ -40,3 +40,7 @@ companyBlacklist:
titleBlacklist:
- word1
- word2
llm_model_type: openai
llm_model: gpt-4o
llm_api_url: https://api.pawan.krd/cosmosrp/v1

View File

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

View File

@ -37,3 +37,7 @@ companyBlacklist:
- Crossover
titleBlacklist:
llm_model_type: openai
llm_model: 'gpt-4o'
llm_api_url: https://api.pawan.krd/cosmosrp/v1'

View File

@ -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

View File

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

17
main.py
View File

@ -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:
@ -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']
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: str, password: str, parameters: dict, openai_api_key: str):
def create_and_run_bot(email, password, parameters, llm_api_key):
try:
style_manager = StyleManager()
resume_generator = ResumeGenerator()
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"))
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: 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(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 = 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)
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")

Binary file not shown.

156
resume_yaml_generator.py Normal file
View File

@ -0,0 +1,156 @@
import argparse
import yaml
from openai import OpenAI
import os
from typing import Dict, Any
import re
from jsonschema import validate, ValidationError
from pdfminer.high_level import extract_text
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 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 <resume_yaml> tags. Only include the YAML content within these tags, without any additional text or code block markers.
"""
response = client.chat.completions.create(
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}
],
temperature=0.5,
)
yaml_content = response.choices[0].message.content.strip()
# Extract YAML content from between the tags
match = re.search(r'<resume_yaml>(.*?)</resume_yaml>', 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)
def pdf_to_text(pdf_path: str) -> str:
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")
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")
# 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)
print(f"Resume YAML generated and saved to {args.output}")
validation_result = validate_yaml(generated_yaml, schema)
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 Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
main()

View File

@ -3,7 +3,8 @@ import os
import re
import textwrap
from datetime import datetime
from typing import Dict, List
from abc import ABC, abstractmethod
from typing import Dict, List, Union
from pathlib import Path
from dotenv import load_dotenv
from langchain_core.messages.ai import AIMessage
@ -17,10 +18,66 @@ import src.strings as strings
load_dotenv()
class AIModel(ABC):
@abstractmethod
def invoke(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 invoke(self, prompt: str) -> str:
print("invoke in openai")
response = self.model.invoke(prompt)
return response
class ClaudeModel(AIModel):
def __init__(self, api_key: str, llm_model: str, llm_api_url: str):
from langchain_anthropic import ChatAnthropic
self.model = ChatAnthropic(model=llm_model, api_key=api_key,
temperature=0.4, base_url=llm_api_url)
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 invoke(self, prompt: str) -> str:
response = self.model.invoke(prompt)
return response
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 invoke(self, prompt: str) -> str:
return self.model.invoke(prompt)
class LLMLogger:
def __init__(self, llm: ChatOpenAI):
def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]):
self.llm = llm
@staticmethod
@ -78,12 +135,12 @@ class LLMLogger:
class LoggerChatModel:
def __init__(self, llm: ChatOpenAI):
def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]):
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 +170,9 @@ 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, config, llm_api_key):
self.ai_adapter = AIAdapter(config, llm_api_key)
self.llm_cheap = LoggerChatModel(self.ai_adapter)
@property
def job_description(self):
return self.job.description

View File

@ -68,18 +68,8 @@ class LinkedInAuthenticator:
print("Security check not completed. Please try again later.")
def is_logged_in(self):
self.driver.get('https://www.linkedin.com/feed')
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, 'share-box-feed-entry__trigger'))
)
buttons = self.driver.find_elements(By.CLASS_NAME, 'share-box-feed-entry__trigger')
if any(button.text.strip() == 'Start a post' for button in buttons):
print("User is already logged in.")
return True
except TimeoutException:
pass
return False
self.driver.get('https://www.linkedin.com/')
return self.driver.current_url == 'https://www.linkedin.com/feed/'
def wait_for_page_load(self, timeout=10):
try:

375
src/linkedin-api.py Normal file
View File

@ -0,0 +1,375 @@
from typing import Dict, List
from linkedin_api import Linkedin
from typing import Optional, Union, Literal
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,
companies: Optional[List[str]] = None,
experience: Optional[
List[
Union[
Literal["1"],
Literal["2"],
Literal["3"],
Literal["4"],
Literal["5"],
Literal["6"],
]
]
] = None,
job_type: Optional[
List[
Union[
Literal["F"],
Literal["C"],
Literal["P"],
Literal["T"],
Literal["I"],
Literal["V"],
Literal["O"],
]
]
] = None,
job_title: Optional[List[str]] = None,
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,
distance: Optional[int] = None,
easy_apply: Optional[bool] = True,
limit=-1,
offset=0,
**kwargs,
) -> List[Dict]:
"""Perform a LinkedIn search for jobs.
:param keywords: Search keywords (str)
:type keywords: str, optional
:param companies: A list of company URN IDs (str)
:type companies: list, optional
:param experience: A list of experience levels, one or many of "1", "2", "3", "4", "5" and "6" (internship, entry level, associate, mid-senior level, director and executive, respectively)
:type experience: list, optional
:param job_type: A list of job types , one or many of "F", "C", "P", "T", "I", "V", "O" (full-time, contract, part-time, temporary, internship, volunteer and "other", respectively)
:type job_type: list, optional
:param job_title: A list of title URN IDs (str)
:type job_title: list, optional
:param industries: A list of industry URN IDs (str)
:type industries: list, optional
:param location_name: Name of the location to search within. Example: "Kyiv City, Ukraine"
: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 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
:type easy_apply: bool, optional. Default value is True.
:param limit: maximum number of results obtained from API queries. -1 means maximum which is defined by constants and is equal to 1000 now.
:type limit: int, optional, default -1
:param offset: indicates how many search results shall be skipped
:type offset: int, optional
:return: List of jobs
:rtype: list
"""
count = Linkedin._MAX_SEARCH_COUNT
if limit is None:
limit = -1
query: Dict[str, Union[str, Dict[str, str]]] = {
"origin": "JOB_SEARCH_PAGE_QUERY_EXPANSION"
}
if keywords:
query["keywords"] = "KEYWORD_PLACEHOLDER"
if location_name:
query["locationFallback"] = "LOCATION_PLACEHOLDER"
query["selectedFilters"] = {}
if companies:
query["selectedFilters"]["company"] = f"List({','.join(companies)})"
if experience:
query["selectedFilters"]["experience"] = f"List({','.join(experience)})"
if job_type:
query["selectedFilters"]["jobType"] = f"List({','.join(job_type)})"
if job_title:
query["selectedFilters"]["title"] = f"List({','.join(job_title)})"
if industries:
query["selectedFilters"]["industry"] = f"List({','.join(industries)})"
if distance:
query["selectedFilters"]["distance"] = f"List({distance})"
if remote:
query["selectedFilters"]["workplaceType"] = f"List({','.join(remote)})"
if easy_apply:
query["selectedFilters"]["applyWithLinkedin"] = "List(true)"
query["selectedFilters"]["timePostedRange"] = f"List(r{listed_at})"
query["spellCorrectionEnabled"] = "true"
query_string = (
str(query)
.replace(" ", "")
.replace("'", "")
.replace("KEYWORD_PLACEHOLDER", keywords or "")
.replace("LOCATION_PLACEHOLDER", location_name or "")
.replace("{", "(")
.replace("}", ")")
)
results = []
while True:
if limit > -1 and limit - len(results) < count:
count = limit - len(results)
default_params = {
"decorationId": "com.linkedin.voyager.dash.deco.jobs.search.JobSearchCardsCollection-174",
"count": count,
"q": "jobSearch",
"query": query_string,
"start": len(results) + offset,
}
res = self._fetch(
f"/voyagerJobsDashJobCards?{urlencode(default_params, safe='(),:')}",
headers={"accept": "application/vnd.linkedin.normalized+json+2.1"},
)
data = res.json()
elements = data.get("included", [])
new_data = []
for e in elements:
trackingUrn = e.get("trackingUrn")
if trackingUrn:
trackingUrn = trackingUrn.split(":")[-1]
e["job_id"] = trackingUrn
if e.get("$type") == "com.linkedin.voyager.dash.jobs.JobPosting":
new_data.append(e)
if not new_data:
break
results.extend(new_data)
if (
(-1 < limit <= len(results))
or len(results) / count >= Linkedin._MAX_REPEATED_REQUESTS
) or len(elements) == 0:
break
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["Accept"] = "application/vnd.linkedin.normalized+json+2.1"
headers["csrf-token"] = cookies["JSESSIONID"].replace('"', "")
headers["Cookie"] = cookie_str
headers["Connection"] = "keep-alive"
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,
)
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:
self.logger.error("Failed to parse JSON response")
return []
form_components = []
for item in data.get("included", []):
if 'formComponent' in item:
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
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="")
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)
break