reformat code
This commit is contained in:
parent
ad85b9587f
commit
5efe6c3048
4 changed files with 82 additions and 98 deletions
26
src/gpt.py
26
src/gpt.py
|
|
@ -3,11 +3,11 @@ import os
|
|||
import re
|
||||
import textwrap
|
||||
import time
|
||||
from datetime import datetime
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, List, Union
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
from typing import Union
|
||||
|
||||
import httpx
|
||||
from Levenshtein import distance
|
||||
|
|
@ -16,39 +16,42 @@ from langchain_core.messages.ai import AIMessage
|
|||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompt_values import StringPromptValue
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_openai import ChatOpenAI
|
||||
|
||||
import src.strings as strings
|
||||
from src.utils import logger
|
||||
|
||||
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)
|
||||
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
|
||||
|
|
@ -58,6 +61,7 @@ class OllamaModel(AIModel):
|
|||
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)
|
||||
|
|
@ -67,7 +71,7 @@ class AIAdapter:
|
|||
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":
|
||||
|
|
@ -80,9 +84,9 @@ class AIAdapter:
|
|||
def invoke(self, prompt: str) -> str:
|
||||
return self.model.invoke(prompt)
|
||||
|
||||
|
||||
class LLMLogger:
|
||||
|
||||
|
||||
def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]):
|
||||
|
||||
self.llm = llm
|
||||
|
|
@ -189,7 +193,6 @@ class LLMLogger:
|
|||
|
||||
class LoggerChatModel:
|
||||
|
||||
|
||||
def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]):
|
||||
|
||||
self.llm = llm
|
||||
|
|
@ -247,7 +250,6 @@ class LoggerChatModel:
|
|||
time.sleep(30)
|
||||
continue
|
||||
|
||||
|
||||
def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]:
|
||||
logger.debug("Parsing LLM result: %s", llmresult)
|
||||
|
||||
|
|
@ -454,12 +456,14 @@ class GPTAnswerer:
|
|||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
output = chain.invoke({"question": question})
|
||||
|
||||
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)
|
||||
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})
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ class LinkedInEasyApplier:
|
|||
|
||||
logger.debug("LinkedInEasyApplier initialized successfully")
|
||||
|
||||
|
||||
def _load_questions_from_json(self) -> List[dict]:
|
||||
output_file = 'answers.json'
|
||||
logger.debug("Loading questions from JSON file: %s", output_file)
|
||||
|
|
@ -60,7 +59,6 @@ class LinkedInEasyApplier:
|
|||
logger.error("Error loading questions data from JSON file: %s", tb_str)
|
||||
raise Exception(f"Error loading questions data from JSON file: \nTraceback:\n{tb_str}")
|
||||
|
||||
|
||||
def check_for_premium_redirect(self, job: Any, max_attempts=3):
|
||||
|
||||
current_url = self.driver.current_url
|
||||
|
|
@ -79,7 +77,6 @@ class LinkedInEasyApplier:
|
|||
raise Exception(
|
||||
f"Redirected to LinkedIn Premium page and failed to return after {max_attempts} attempts. Job application aborted.")
|
||||
|
||||
|
||||
def job_apply(self, job: Any):
|
||||
logger.debug("Starting job application for job: %s", job)
|
||||
|
||||
|
|
@ -167,7 +164,6 @@ class LinkedInEasyApplier:
|
|||
|
||||
if method.get('find_elements'):
|
||||
|
||||
|
||||
buttons = self.driver.find_elements(By.XPATH, method['xpath'])
|
||||
if buttons:
|
||||
for index, button in enumerate(buttons):
|
||||
|
|
@ -209,7 +205,6 @@ class LinkedInEasyApplier:
|
|||
logger.error("No clickable 'Easy Apply' button found after 2 attempts. Page source:\n%s", page_source)
|
||||
raise Exception("No clickable 'Easy Apply' button found")
|
||||
|
||||
|
||||
def _get_job_description(self) -> str:
|
||||
logger.debug("Getting job description")
|
||||
try:
|
||||
|
|
@ -541,7 +536,6 @@ class LinkedInEasyApplier:
|
|||
wrapped_lines.append(line)
|
||||
return wrapped_lines
|
||||
|
||||
|
||||
lines = split_text_by_width(cover_letter_text, "Helvetica", 12, max_width)
|
||||
|
||||
for line in lines:
|
||||
|
|
@ -567,7 +561,6 @@ class LinkedInEasyApplier:
|
|||
logger.error(f"Traceback: {tb_str}")
|
||||
raise
|
||||
|
||||
|
||||
file_size = os.path.getsize(file_path_pdf)
|
||||
max_file_size = 2 * 1024 * 1024 # 2 MB
|
||||
logger.debug(f"Cover letter file size: {file_size} bytes")
|
||||
|
|
@ -670,7 +663,6 @@ class LinkedInEasyApplier:
|
|||
|
||||
for item in self.all_data:
|
||||
|
||||
|
||||
logger.debug(
|
||||
f"Comparing sanitized stored question: '{self._sanitize_text(item['question'])}' and type: '{item.get('type')}' with current question: '{self._sanitize_text(question_text)}' and type: '{question_type}'")
|
||||
|
||||
|
|
@ -697,7 +689,6 @@ class LinkedInEasyApplier:
|
|||
answer = self.gpt_answerer.answer_question_textual_wide_range(question_text)
|
||||
logger.debug(f"Generated textual answer: {answer}")
|
||||
|
||||
|
||||
self._save_questions_to_json({'type': question_type, 'question': question_text, 'answer': answer})
|
||||
self._enter_text(text_field, answer)
|
||||
logger.debug("Entered new answer into the textbox and saved it to JSON.")
|
||||
|
|
@ -730,7 +721,6 @@ class LinkedInEasyApplier:
|
|||
logger.debug("Entered existing date answer")
|
||||
return True
|
||||
|
||||
|
||||
self._save_questions_to_json({'type': 'date', 'question': question_text, 'answer': answer_text})
|
||||
self._enter_text(date_field, answer_text)
|
||||
logger.debug("Entered new date answer")
|
||||
|
|
@ -752,7 +742,6 @@ class LinkedInEasyApplier:
|
|||
|
||||
logger.debug(f"Dropdown options found: {options}")
|
||||
|
||||
|
||||
question_text = question.find_element(By.TAG_NAME, 'label').text.lower()
|
||||
logger.debug(f"Processing dropdown or combobox question: {question_text}")
|
||||
|
||||
|
|
|
|||
|
|
@ -452,9 +452,9 @@ class LinkedInJobManager:
|
|||
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...")
|
||||
utils.printyellow(
|
||||
f"Already applied at {company} (once per company policy), skipping...")
|
||||
return True
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
|
|
|||
|
|
@ -1,58 +1,59 @@
|
|||
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
|
||||
from typing import Dict, List
|
||||
from typing import Optional, Union, Literal
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from linkedin_api import Linkedin
|
||||
|
||||
# set log to all debug
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class LinkedInEvolvedAPI(Linkedin):
|
||||
already_applied_jobs: List[str] = []
|
||||
|
||||
|
||||
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"],
|
||||
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_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: None | int = None,
|
||||
distance: Optional[int] = None,
|
||||
easy_apply: Optional[bool] = True,
|
||||
limit=-1,
|
||||
offset=0,
|
||||
**kwargs,
|
||||
] = 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: None | int = None,
|
||||
distance: Optional[int] = None,
|
||||
easy_apply: Optional[bool] = True,
|
||||
limit=-1,
|
||||
offset=0,
|
||||
**kwargs,
|
||||
) -> List[Dict]:
|
||||
"""Perform a LinkedIn search for jobs.
|
||||
|
||||
|
|
@ -154,21 +155,21 @@ class LinkedInEvolvedAPI(Linkedin):
|
|||
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
|
||||
(-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]:
|
||||
|
||||
def get_fields_for_easy_apply(self, job_id: str) -> List[Dict]:
|
||||
"""Get fields needed for easy apply jobs.
|
||||
|
||||
:param job_id: Job ID
|
||||
|
|
@ -181,14 +182,12 @@ class LinkedInEvolvedAPI(Linkedin):
|
|||
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}",
|
||||
|
|
@ -217,26 +216,26 @@ class LinkedInEvolvedAPI(Linkedin):
|
|||
except ValueError:
|
||||
self.logger.error("Failed to parse JSON response")
|
||||
return []
|
||||
|
||||
|
||||
form_components = []
|
||||
|
||||
for item in data.get("included", []):
|
||||
if 'formComponent' in item:
|
||||
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']
|
||||
|
|
@ -244,18 +243,18 @@ class LinkedInEvolvedAPI(Linkedin):
|
|||
component_info['selectableOptions'] = options
|
||||
elif 'selectableOptions' in form_component_details:
|
||||
options = [
|
||||
opt['textSelectableOption']['optionText']['text']
|
||||
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:
|
||||
|
||||
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)
|
||||
|
|
@ -265,11 +264,11 @@ class LinkedInEvolvedAPI(Linkedin):
|
|||
# {'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": [
|
||||
# {
|
||||
|
|
@ -349,23 +348,19 @@ class LinkedInEvolvedAPI(Linkedin):
|
|||
# }
|
||||
# ],
|
||||
# "trackingId": ""
|
||||
#}
|
||||
# }
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## EXAMPLE USAGE
|
||||
if __name__ == "__main__":
|
||||
api: LinkedInEvolvedAPI = LinkedInEvolvedAPI(username="", password="")
|
||||
jobs = api.search_jobs(keywords="Frontend Developer", location_name="Italia", limit=100, easy_apply=True, offset=1, listed_at=None)
|
||||
api: LinkedInEvolvedAPI = LinkedInEvolvedAPI(username="", password="")
|
||||
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}")
|
||||
|
|
@ -379,7 +374,3 @@ if __name__ == "__main__":
|
|||
for field in fields:
|
||||
print(field)
|
||||
break
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue