From 91c7b17e30180e95d37154dca17dc8c8d9a05035 Mon Sep 17 00:00:00 2001 From: Ivan Date: Tue, 5 Nov 2024 16:03:55 +0400 Subject: [PATCH 01/27] Added integration with aiml --- README.md | 2 + src/llm/llm_manager.py | 251 ++++++++++++++++++++++++++--------------- 2 files changed, 162 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index a1e34dd9..1fdb353e 100644 --- a/README.md +++ b/README.md @@ -250,12 +250,14 @@ This file defines your job search parameters and bot behavior. Each section cont - ollama: llama2, mistral:v0.3 - claude: any model - gemini: any model + - aiml: any model - `llm_api_url`: - Link of the API endpoint for the LLM model - openai: - ollama: - claude: - gemini: + - aiml: - Note: To run local Ollama, follow the guidelines here: [Guide to Ollama deployment](https://github.com/ollama/ollama) ### 3. plain_text_resume.yaml diff --git a/src/llm/llm_manager.py b/src/llm/llm_manager.py index a96da271..113193fc 100644 --- a/src/llm/llm_manager.py +++ b/src/llm/llm_manager.py @@ -6,20 +6,19 @@ from abc import ABC, abstractmethod from datetime import datetime from pathlib import Path -from typing import Dict, List -from typing import Union +from typing import Dict, List, Union import httpx -from Levenshtein import distance from dotenv import load_dotenv from langchain_core.messages import BaseMessage 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 Levenshtein import distance +from loguru import logger import src.strings as strings -from loguru import logger load_dotenv() @@ -33,8 +32,10 @@ def invoke(self, prompt: str) -> str: class OpenAIModel(AIModel): def __init__(self, api_key: str, llm_model: str): from langchain_openai import ChatOpenAI - self.model = ChatOpenAI(model_name=llm_model, openai_api_key=api_key, - temperature=0.4) + + self.model = ChatOpenAI( + model_name=llm_model, openai_api_key=api_key, temperature=0.4 + ) def invoke(self, prompt: str) -> BaseMessage: logger.debug("Invoking OpenAI API") @@ -42,11 +43,29 @@ def invoke(self, prompt: str) -> BaseMessage: return response +class AIMLModel(AIModel): + def __init__(self, api_key: str, llm_model: str): + from langchain_openai import ChatOpenAI + + self.base_url = "https://api.aimlapi.com/v2" + self.model = ChatOpenAI( + model_name=llm_model, + openai_api_key=api_key, + temperature=0.7, + base_url=self.base_url, + ) + + def invoke(self, prompt: str) -> BaseMessage: + logger.debug("Invoking AIML API") + response = self.model.invoke(prompt) + return response + + class ClaudeModel(AIModel): def __init__(self, api_key: str, llm_model: str): from langchain_anthropic import ChatAnthropic - self.model = ChatAnthropic(model=llm_model, api_key=api_key, - temperature=0.4) + + self.model = ChatAnthropic(model=llm_model, api_key=api_key, temperature=0.4) def invoke(self, prompt: str) -> BaseMessage: response = self.model.invoke(prompt) @@ -68,55 +87,71 @@ def invoke(self, prompt: str) -> BaseMessage: response = self.model.invoke(prompt) return response -#gemini doesn't seem to work because API doesn't rstitute answers for questions that involve answers that are too short + +# gemini doesn't seem to work because API doesn't rstitute answers for questions that involve answers that are too short class GeminiModel(AIModel): - def __init__(self, api_key:str, llm_model: str): - from langchain_google_genai import ChatGoogleGenerativeAI, HarmBlockThreshold, HarmCategory - self.model = ChatGoogleGenerativeAI(model=llm_model, google_api_key=api_key,safety_settings={ - HarmCategory.HARM_CATEGORY_UNSPECIFIED: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DEROGATORY: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_TOXICITY: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_VIOLENCE: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUAL: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_MEDICAL: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, - HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE - }) + def __init__(self, api_key: str, llm_model: str): + from langchain_google_genai import ( + ChatGoogleGenerativeAI, + HarmBlockThreshold, + HarmCategory, + ) + + self.model = ChatGoogleGenerativeAI( + model=llm_model, + google_api_key=api_key, + safety_settings={ + HarmCategory.HARM_CATEGORY_UNSPECIFIED: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_DEROGATORY: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_TOXICITY: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_VIOLENCE: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_SEXUAL: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_MEDICAL: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_DANGEROUS: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE, + HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE, + }, + ) def invoke(self, prompt: str) -> BaseMessage: response = self.model.invoke(prompt) return response + class HuggingFaceModel(AIModel): def __init__(self, api_key: str, llm_model: str): - from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace - self.model = HuggingFaceEndpoint(repo_id=llm_model, huggingfacehub_api_token=api_key, - temperature=0.4) - self.chatmodel=ChatHuggingFace(llm=self.model) + from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint + + self.model = HuggingFaceEndpoint( + repo_id=llm_model, huggingfacehub_api_token=api_key, temperature=0.4 + ) + self.chatmodel = ChatHuggingFace(llm=self.model) def invoke(self, prompt: str) -> BaseMessage: response = self.chatmodel.invoke(prompt) logger.debug("Invoking Model from Hugging Face API") - print(response,type(response)) + print(response, type(response)) 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_model_type = config["llm_model_type"] + llm_model = config["llm_model"] - llm_api_url = config.get('llm_api_url', "") + llm_api_url = config.get("llm_api_url", "") logger.debug(f"Using {llm_model_type} with {llm_model}") if llm_model_type == "openai": return OpenAIModel(api_key, llm_model) + elif llm_model_type == "aiml": + return AIMLModel(api_key, llm_model) elif llm_model_type == "claude": return ClaudeModel(api_key, llm_model) elif llm_model_type == "ollama": @@ -124,7 +159,7 @@ def _create_model(self, config: dict, api_key: str) -> AIModel: elif llm_model_type == "gemini": return GeminiModel(api_key, llm_model) elif llm_model_type == "huggingface": - return HuggingFaceModel(api_key, llm_model) + return HuggingFaceModel(api_key, llm_model) else: raise ValueError(f"Unsupported model type: {llm_model_type}") @@ -133,8 +168,9 @@ def invoke(self, prompt: str) -> str: class LLMLogger: - - def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel]): + def __init__( + self, llm: Union[OpenAIModel, AIMLModel, OllamaModel, ClaudeModel, GeminiModel] + ): self.llm = llm logger.debug(f"LLMLogger successfully initialized with LLM: {llm}") @@ -145,8 +181,7 @@ def log_request(prompts, parsed_reply: Dict[str, Dict]): logger.debug(f"Parsed reply received: {parsed_reply}") try: - calls_log = os.path.join( - Path("data_folder/output"), "open_ai_calls.json") + calls_log = os.path.join(Path("data_folder/output"), "open_ai_calls.json") logger.debug(f"Logging path determined: {calls_log}") except Exception as e: logger.error(f"Error determining the log path: {str(e)}") @@ -174,7 +209,9 @@ def log_request(prompts, parsed_reply: Dict[str, Dict]): f"prompt_{i + 1}": prompt.content for i, prompt in enumerate(prompts.messages) } - logger.debug(f"Prompts converted to dictionary using default method: {prompts}") + logger.debug( + f"Prompts converted to dictionary using default method: {prompts}" + ) except Exception as e: logger.error(f"Error converting prompts using default method: {str(e)}") raise @@ -191,7 +228,9 @@ def log_request(prompts, parsed_reply: Dict[str, Dict]): output_tokens = token_usage["output_tokens"] input_tokens = token_usage["input_tokens"] total_tokens = token_usage["total_tokens"] - logger.debug(f"Token usage - Input: {input_tokens}, Output: {output_tokens}, Total: {total_tokens}") + logger.debug( + f"Token usage - Input: {input_tokens}, Output: {output_tokens}, Total: {total_tokens}" + ) except KeyError as e: logger.error(f"KeyError in parsed_reply structure: {str(e)}") raise @@ -206,8 +245,9 @@ def log_request(prompts, parsed_reply: Dict[str, Dict]): try: prompt_price_per_token = 0.00000015 completion_price_per_token = 0.0000006 - total_cost = (input_tokens * prompt_price_per_token) + \ - (output_tokens * completion_price_per_token) + total_cost = (input_tokens * prompt_price_per_token) + ( + output_tokens * completion_price_per_token + ) logger.debug(f"Total cost calculated: {total_cost}") except Exception as e: logger.error(f"Error calculating total cost: {str(e)}") @@ -226,13 +266,14 @@ def log_request(prompts, parsed_reply: Dict[str, Dict]): } logger.debug(f"Log entry created: {log_entry}") except KeyError as e: - logger.error(f"Error creating log entry: missing key {str(e)} in parsed_reply") + logger.error( + f"Error creating log entry: missing key {str(e)} in parsed_reply" + ) raise try: with open(calls_log, "a", encoding="utf-8") as f: - json_string = json.dumps( - log_entry, ensure_ascii=False, indent=4) + json_string = json.dumps(log_entry, ensure_ascii=False, indent=4) f.write(json_string + "\n") logger.debug(f"Log entry written to file: {calls_log}") except Exception as e: @@ -241,7 +282,6 @@ def log_request(prompts, parsed_reply: Dict[str, Dict]): class LoggerChatModel: - def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel]): self.llm = llm logger.debug(f"LoggerChatModel successfully initialized with LLM: {llm}") @@ -258,8 +298,7 @@ def __call__(self, messages: List[Dict[str, str]]) -> str: parsed_reply = self.parse_llmresult(reply) logger.debug(f"Parsed LLM reply: {parsed_reply}") - LLMLogger.log_request( - prompts=messages, parsed_reply=parsed_reply) + LLMLogger.log_request(prompts=messages, parsed_reply=parsed_reply) logger.debug("Request successfully logged") return reply @@ -267,32 +306,38 @@ def __call__(self, messages: List[Dict[str, str]]) -> str: except httpx.HTTPStatusError as e: logger.error(f"HTTPStatusError encountered: {str(e)}") if e.response.status_code == 429: - retry_after = e.response.headers.get('retry-after') - retry_after_ms = e.response.headers.get('retry-after-ms') + retry_after = e.response.headers.get("retry-after") + retry_after_ms = e.response.headers.get("retry-after-ms") if retry_after: wait_time = int(retry_after) logger.warning( - f"Rate limit exceeded. Waiting for {wait_time} seconds before retrying (extracted from 'retry-after' header)...") + f"Rate limit exceeded. Waiting for {wait_time} seconds before retrying (extracted from 'retry-after' header)..." + ) time.sleep(wait_time) elif retry_after_ms: wait_time = int(retry_after_ms) / 1000.0 logger.warning( - f"Rate limit exceeded. Waiting for {wait_time} seconds before retrying (extracted from 'retry-after-ms' header)...") + f"Rate limit exceeded. Waiting for {wait_time} seconds before retrying (extracted from 'retry-after-ms' header)..." + ) time.sleep(wait_time) else: wait_time = 30 logger.warning( - f"'retry-after' header not found. Waiting for {wait_time} seconds before retrying (default)...") + f"'retry-after' header not found. Waiting for {wait_time} seconds before retrying (default)..." + ) time.sleep(wait_time) else: - logger.error(f"HTTP error occurred with status code: {e.response.status_code}, waiting 30 seconds before retrying") + logger.error( + f"HTTP error occurred with status code: {e.response.status_code}, waiting 30 seconds before retrying" + ) time.sleep(30) except Exception as e: logger.error(f"Unexpected error occurred: {str(e)}") logger.info( - "Waiting for 30 seconds before retrying due to an unexpected error.") + "Waiting for 30 seconds before retrying due to an unexpected error." + ) time.sleep(30) continue @@ -300,7 +345,7 @@ def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]: logger.debug(f"Parsing LLM result: {llmresult}") try: - if hasattr(llmresult, 'usage_metadata'): + if hasattr(llmresult, "usage_metadata"): content = llmresult.content response_metadata = llmresult.response_metadata id_ = llmresult.id @@ -310,7 +355,9 @@ def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]: "content": content, "response_metadata": { "model_name": response_metadata.get("model_name", ""), - "system_fingerprint": response_metadata.get("system_fingerprint", ""), + "system_fingerprint": response_metadata.get( + "system_fingerprint", "" + ), "finish_reason": response_metadata.get("finish_reason", ""), "logprobs": response_metadata.get("logprobs", None), }, @@ -321,11 +368,11 @@ def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]: "total_tokens": usage_metadata.get("total_tokens", 0), }, } - else : + else: content = llmresult.content response_metadata = llmresult.response_metadata id_ = llmresult.id - token_usage = response_metadata['token_usage'] + token_usage = response_metadata["token_usage"] parsed_result = { "content": content, @@ -339,23 +386,20 @@ def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]: "output_tokens": token_usage.completion_tokens, "total_tokens": token_usage.total_tokens, }, - } + } logger.debug(f"Parsed LLM result successfully: {parsed_result}") return parsed_result except KeyError as e: - logger.error( - f"KeyError while parsing LLM result: missing key {str(e)}") + logger.error(f"KeyError while parsing LLM result: missing key {str(e)}") raise except Exception as e: - logger.error( - f"Unexpected error while parsing LLM result: {str(e)}") + logger.error(f"Unexpected error while parsing LLM result: {str(e)}") raise class GPTAnswerer: - def __init__(self, config, llm_api_key): self.ai_adapter = AIAdapter(config, llm_api_key) self.llm_cheap = LoggerChatModel(self.ai_adapter) @@ -393,7 +437,8 @@ def set_job(self, job): logger.debug(f"Setting job: {job}") self.job = job self.job.set_summarize_job_description( - self.summarize_job_description(self.job.description)) + self.summarize_job_description(self.job.description) + ) def set_job_application_profile(self, job_application_profile): logger.debug(f"Setting job application profile: {job_application_profile}") @@ -404,8 +449,7 @@ def summarize_job_description(self, text: str) -> str: strings.summarize_prompt_template = self._preprocess_template_string( strings.summarize_prompt_template ) - prompt = ChatPromptTemplate.from_template( - strings.summarize_prompt_template) + prompt = ChatPromptTemplate.from_template(strings.summarize_prompt_template) chain = prompt | self.llm_cheap | StrOutputParser() output = chain.invoke({"text": text}) logger.debug(f"Summary generated: {output}") @@ -419,15 +463,25 @@ def _create_chain(self, template: str): def answer_question_textual_wide_range(self, question: str) -> str: logger.debug(f"Answering textual question: {question}") chains = { - "personal_information": self._create_chain(strings.personal_information_template), - "self_identification": self._create_chain(strings.self_identification_template), - "legal_authorization": self._create_chain(strings.legal_authorization_template), + "personal_information": self._create_chain( + strings.personal_information_template + ), + "self_identification": self._create_chain( + strings.self_identification_template + ), + "legal_authorization": self._create_chain( + strings.legal_authorization_template + ), "work_preferences": self._create_chain(strings.work_preferences_template), "education_details": self._create_chain(strings.education_details_template), - "experience_details": self._create_chain(strings.experience_details_template), + "experience_details": self._create_chain( + strings.experience_details_template + ), "projects": self._create_chain(strings.projects_template), "availability": self._create_chain(strings.availability_template), - "salary_expectations": self._create_chain(strings.salary_expectations_template), + "salary_expectations": self._create_chain( + strings.salary_expectations_template + ), "certifications": self._create_chain(strings.certifications_template), "languages": self._create_chain(strings.languages_template), "interests": self._create_chain(strings.interests_template), @@ -528,50 +582,64 @@ def answer_question_textual_wide_range(self, question: str) -> str: r"(Personal information|Self Identification|Legal Authorization|Work Preferences|Education " r"Details|Experience Details|Projects|Availability|Salary " r"Expectations|Certifications|Languages|Interests|Cover letter)", - output, re.IGNORECASE) + output, + re.IGNORECASE, + ) if not match: - raise ValueError( - "Could not extract section name from the response.") + 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}) + {"resume": self.resume, "job_description": self.job_description} + ) logger.debug(f"Cover letter generated: {output}") return output - resume_section = getattr(self.resume, section_name, None) or getattr(self.job_application_profile, section_name, - None) + resume_section = getattr(self.resume, section_name, None) or getattr( + self.job_application_profile, section_name, None + ) if resume_section is None: logger.error( - f"Section '{section_name}' not found in either resume or job_application_profile.") - raise ValueError(f"Section '{section_name}' not found in either resume or job_application_profile.") + f"Section '{section_name}' not found in either resume or job_application_profile." + ) + raise ValueError( + f"Section '{section_name}' not found in either resume or job_application_profile." + ) chain = chains.get(section_name) if chain is None: logger.error(f"Chain not defined for section '{section_name}'") raise ValueError(f"Chain not defined for section '{section_name}'") - output = chain.invoke( - {"resume_section": resume_section, "question": question}) + output = chain.invoke({"resume_section": resume_section, "question": question}) logger.debug(f"Question answered: {output}") return output - def answer_question_numeric(self, question: str, default_experience: str = 3) -> str: + def answer_question_numeric( + self, question: str, default_experience: str = 3 + ) -> str: logger.debug(f"Answering numeric question: {question}") func_template = self._preprocess_template_string( - strings.numeric_question_template) + strings.numeric_question_template + ) prompt = ChatPromptTemplate.from_template(func_template) chain = prompt | self.llm_cheap | StrOutputParser() output_str = chain.invoke( - {"resume_educations": self.resume.education_details, "resume_jobs": self.resume.experience_details, - "resume_projects": self.resume.projects, "question": question}) + { + "resume_educations": self.resume.education_details, + "resume_jobs": self.resume.experience_details, + "resume_projects": self.resume.projects, + "question": question, + } + ) logger.debug(f"Raw output for numeric question: {output_str}") try: output = self.extract_number_from_string(output_str) logger.debug(f"Extracted number: {output}") except ValueError: logger.warning( - f"Failed to extract number, using default experience: {default_experience}") + f"Failed to extract number, using default experience: {default_experience}" + ) output = default_experience return output @@ -587,12 +655,12 @@ def extract_number_from_string(self, output_str): def answer_question_from_options(self, question: str, options: list[str]) -> str: logger.debug(f"Answering question from options: {question}") - func_template = self._preprocess_template_string( - strings.options_template) + func_template = self._preprocess_template_string(strings.options_template) prompt = ChatPromptTemplate.from_template(func_template) chain = prompt | self.llm_cheap | StrOutputParser() output_str = chain.invoke( - {"resume": self.resume, "question": question, "options": options}) + {"resume": self.resume, "question": question, "options": options} + ) logger.debug(f"Raw output for options question: {output_str}") best_option = self.find_best_match(output_str, options) logger.debug(f"Best option determined: {best_option}") @@ -600,7 +668,8 @@ def answer_question_from_options(self, question: str, options: list[str]) -> str def resume_or_cover(self, phrase: str) -> str: logger.debug( - f"Determining if phrase refers to resume or cover letter: {phrase}") + f"Determining if phrase refers to resume or cover letter: {phrase}" + ) prompt_template = """ Given the following phrase, respond with only 'resume' if the phrase is about a resume, or 'cover' if it's about a cover letter. If the phrase contains only one word 'upload', consider it as 'cover'. From e3b861cc88b0fe8dba17528070ab99b67bf34540 Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 11 Nov 2024 12:19:29 -0500 Subject: [PATCH 02/27] Update README.md --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1fdb353e..77599329 100644 --- a/README.md +++ b/README.md @@ -251,13 +251,11 @@ This file defines your job search parameters and bot behavior. Each section cont - claude: any model - gemini: any model - aiml: any model + - `llm_api_url`: - - Link of the API endpoint for the LLM model - - openai: + - Link of the API endpoint for the LLM model. (only requried for ollama) - ollama: - - claude: - - gemini: - - aiml: + - Note: To run local Ollama, follow the guidelines here: [Guide to Ollama deployment](https://github.com/ollama/ollama) ### 3. plain_text_resume.yaml From b64eb03a845b536e05bfb87947fb241870427489 Mon Sep 17 00:00:00 2001 From: Ved Gupta Date: Tue, 12 Nov 2024 18:40:31 +0530 Subject: [PATCH 03/27] Add GroqAIModel support --- requirements.txt | 1 + src/ai_hawk/llm/llm_manager.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index acd912e0..7f479286 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ jsonschema==4.23.0 jsonschema-specifications==2023.12.1 langchain==0.2.11 langchain-anthropic +langchain-groq langchain-huggingface langchain-community==0.2.10 langchain-core===0.2.36 diff --git a/src/ai_hawk/llm/llm_manager.py b/src/ai_hawk/llm/llm_manager.py index b5d5be4b..367f1607 100644 --- a/src/ai_hawk/llm/llm_manager.py +++ b/src/ai_hawk/llm/llm_manager.py @@ -29,6 +29,16 @@ class AIModel(ABC): def invoke(self, prompt: str) -> str: pass +class GroqAIModel(AIModel): + def __init__(self, api_key: str, llm_model: str): + from langchain_groq import ChatGroq + self.model = ChatGroq(model=llm_model, api_key=api_key, + temperature=0.4) + + def invoke(self, prompt: str) -> BaseMessage: + response = self.model.invoke(prompt) + logger.debug("Invoking GroqAI API") + return response class OpenAIModel(AIModel): def __init__(self, api_key: str, llm_model: str): @@ -123,7 +133,9 @@ def _create_model(self, config: dict, api_key: str) -> AIModel: elif llm_model_type == "gemini": return GeminiModel(api_key, llm_model) elif llm_model_type == "huggingface": - return HuggingFaceModel(api_key, llm_model) + return HuggingFaceModel(api_key, llm_model) + elif llm_model_type == "groq": + return GroqAIModel(api_key, llm_model) else: raise ValueError(f"Unsupported model type: {llm_model_type}") @@ -133,7 +145,7 @@ def invoke(self, prompt: str) -> str: class LLMLogger: - def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel]): + def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel, GroqAIModel]): self.llm = llm logger.debug(f"LLMLogger successfully initialized with LLM: {llm}") @@ -241,7 +253,7 @@ def log_request(prompts, parsed_reply: Dict[str, Dict]): class LoggerChatModel: - def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel]): + def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel, GroqAIModel]): self.llm = llm logger.debug(f"LoggerChatModel successfully initialized with LLM: {llm}") From b8d7ffa6202eee3756d2cc735e3f59f31dc9040d Mon Sep 17 00:00:00 2001 From: Ved Gupta Date: Wed, 13 Nov 2024 08:44:19 +0530 Subject: [PATCH 04/27] README.md updated for GROQ API --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f7be1b43..7d98d949 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ Auto_Jobs_Applier_AIHawk steps in as a game-changing solution to these challenge This file contains sensitive information. Never share or commit this file to version control. -- `llm_api_key: [Your OpenAI or Ollama API key or Gemini API key]` +- `llm_api_key: [Your OpenAI or Ollama API key or Gemini API key or Groq API key]` - Replace with your OpenAI API key for GPT integration - To obtain an API key, follow the tutorial at: - 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). @@ -158,6 +158,7 @@ This file contains sensitive information. Never share or commit this file to ver 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). - For obtaining Gemini API key visit [Google AI for Devs](https://ai.google.dev/gemini-api/docs/api-key) + - For obtaining Groq API key visit [Groq API](https://api.groq.com/v1) ### 2. config.yaml @@ -235,19 +236,21 @@ This file defines your job search parameters and bot behavior. Each section cont #### 2.1 config.yaml - Customize LLM model endpoint - `llm_model_type`: - - Choose the model type, supported: openai / ollama / claude / gemini + - Choose the model type, supported: openai / ollama / claude / gemini / groq - `llm_model`: - Choose the LLM model, currently supported: - openai: gpt-4o - ollama: llama2, mistral:v0.3 - claude: any model - gemini: any model + - groq: llama3-groq-70b-8192-tool-use-preview, llama3-groq-8b-8192-tool-use-preview, llama-3.1-70b-versatile, llama-3.1-8b-instant, llama-3.2-3b-preview, llama3-70b-8192, llama3-8b-8192, mixtral-8x7b-32768 - `llm_api_url`: - Link of the API endpoint for the LLM model - openai: - ollama: - claude: - gemini: + - groq: - Note: To run local Ollama, follow the guidelines here: [Guide to Ollama deployment](https://github.com/ollama/ollama) ### 3. plain_text_resume.yaml From 3a9e5b70621455179682c3ef501b25c1fd7727d8 Mon Sep 17 00:00:00 2001 From: Akhil Date: Tue, 12 Nov 2024 22:26:40 -0500 Subject: [PATCH 05/27] Update llm_manager.py repalced union with base class. --- src/ai_hawk/llm/llm_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ai_hawk/llm/llm_manager.py b/src/ai_hawk/llm/llm_manager.py index 367f1607..41c1d4df 100644 --- a/src/ai_hawk/llm/llm_manager.py +++ b/src/ai_hawk/llm/llm_manager.py @@ -145,7 +145,7 @@ def invoke(self, prompt: str) -> str: class LLMLogger: - def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel, GroqAIModel]): + def __init__(self, llm: AIModel): self.llm = llm logger.debug(f"LLMLogger successfully initialized with LLM: {llm}") @@ -253,7 +253,7 @@ def log_request(prompts, parsed_reply: Dict[str, Dict]): class LoggerChatModel: - def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel, GeminiModel, GroqAIModel]): + def __init__(self, llm: AIModel): self.llm = llm logger.debug(f"LoggerChatModel successfully initialized with LLM: {llm}") @@ -642,4 +642,4 @@ def is_job_suitable(self): logger.info(f"Job suitability score: {score}") if int(score) < 7 : logger.debug(f"Job is not suitable: {reasoning}") - return int(score) >= 7 \ No newline at end of file + return int(score) >= 7 From 62802962bc7392172da872991f0dd0be98ce90cf Mon Sep 17 00:00:00 2001 From: Ved Gupta Date: Thu, 14 Nov 2024 01:52:00 +0000 Subject: [PATCH 06/27] groq added in constants --- constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/constants.py b/constants.py index c750c04b..adecef03 100644 --- a/constants.py +++ b/constants.py @@ -68,5 +68,6 @@ CLAUDE = "claude" OLLAMA = "ollama" GEMINI = "gemini" +GROQ = "groq" HUGGINGFACE = "huggingface" PERPLEXITY = "perplexity" From 306fca678b918afe3ac4775094f82311ff4c11d2 Mon Sep 17 00:00:00 2001 From: Ved Gupta Date: Thu, 14 Nov 2024 10:43:26 +0000 Subject: [PATCH 07/27] just groq version added in requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f479286..f92a8898 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ jsonschema==4.23.0 jsonschema-specifications==2023.12.1 langchain==0.2.11 langchain-anthropic -langchain-groq +langchain-groq==0.1.9 langchain-huggingface langchain-community==0.2.10 langchain-core===0.2.36 From 0bc062473f5f68f77b7fea4ecef798d1ac79ae15 Mon Sep 17 00:00:00 2001 From: Federico <85809106+feder-cr@users.noreply.github.com> Date: Fri, 15 Nov 2024 20:56:29 +0100 Subject: [PATCH 08/27] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0e853fa5..62083354 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Join our community: [Telegram](https://t.me/AIhawkCommunity) (for Normal user) | -**Creator** [feder-cr](https://github.com/feder-cr), Co-Founder of Ai Hawk
-As AI Hawk is focusing on their proprietary product - solving problems in hiring for companies, currently this project is led, managed, and maintained by a group of open-source contributors, with a focus on building tools to help job seekers land the jobs they deserve. +**Creator** [feder-cr](https://github.com/feder-cr), Co-Founder of AIHawk
+As AIHawk is focusing on their proprietary product - solving problems in hiring for companies, currently this project is led, managed, and maintained by a group of open-source contributors, with a focus on building tools to help job seekers land the jobs they deserve. **Project Maintainers / Leads**: [surapuramakhil](https://github.com/surapuramakhil), [sarob](https://github.com/sarob), [cjbbb](https://github.com/cjbbb) From fd69fc0587a51a1377d3eb9d0d0ead93876f49dd Mon Sep 17 00:00:00 2001 From: Yauheni Muravitski Date: Sun, 17 Nov 2024 00:02:29 +0400 Subject: [PATCH 09/27] perf: :zap: Optimize prompt for better caching Some providers (as OpenAI) support reducing cost of calls by caching repetitive content, but the feature works only with the initial portion (prefix) of prompts --- src/ai_hawk/llm/prompts.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ai_hawk/llm/prompts.py b/src/ai_hawk/llm/prompts.py index cc7bc80a..59586b2a 100644 --- a/src/ai_hawk/llm/prompts.py +++ b/src/ai_hawk/llm/prompts.py @@ -262,17 +262,17 @@ - Do not include any introductions, explanations, or additional information. - The letter should be formatted into paragraph. +## My resume: +``` +{resume} +``` + ## Company Name: {company} - ## Job Description: ``` {job_description} ``` -## My resume: -``` -{resume} -``` """ numeric_question_template = """ @@ -432,10 +432,10 @@ is_relavant_position_template = """ Evaluate whether the provided resume meets the requirements outlined in the job description. Determine if the candidate is suitable for the job based on the information provided. -Job Description: {job_description} - Resume: {resume} +Job Description: {job_description} + Instructions: 1. Extract the key requirements from the job description, identifying hard requirements (must-haves) and soft requirements (nice-to-haves). 2. Identify the relevant qualifications from the resume. From 0fb1d128e84beb6b28bb1fb0b858801526f9d26b Mon Sep 17 00:00:00 2001 From: Saimon Tsegai <85457358+49Simon@users.noreply.github.com> Date: Sun, 17 Nov 2024 16:51:08 -0500 Subject: [PATCH 10/27] fix: robust handling of score and reasoning parse --- src/ai_hawk/llm/llm_manager.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ai_hawk/llm/llm_manager.py b/src/ai_hawk/llm/llm_manager.py index 209cc7b1..b18f6f98 100644 --- a/src/ai_hawk/llm/llm_manager.py +++ b/src/ai_hawk/llm/llm_manager.py @@ -695,8 +695,14 @@ def is_job_suitable(self): ) output = self._clean_llm_output(raw_output) logger.debug(f"Job suitability output: {output}") - score = re.search(r"Score: (\d+)", output).group(1) - reasoning = re.search(r"Reasoning: (.+)", output, re.DOTALL).group(1) + + try: + score = re.search(r"Score:\s*(\d+)", output, re.IGNORECASE).group(1) + reasoning = re.search(r"Reasoning:\s*(.+)", output, re.IGNORECASE | re.DOTALL).group(1) + except AttributeError: + logger.warning("Failed to extract score or reasoning from LLM. Proceeding with application, but job may or may not be suitable.") + return True + logger.info(f"Job suitability score: {score}") if int(score) < JOB_SUITABILITY_SCORE: logger.debug(f"Job is not suitable: {reasoning}") From c5955bdd1295df232cd5f6a63df4e50e00a3f524 Mon Sep 17 00:00:00 2001 From: "_.okti" <54273355+OctavianTheI@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:07:34 +0100 Subject: [PATCH 11/27] Add AI/ML API info included links to AI/ML API docs and the endpoint to be used in the code. --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f8878a21..cc6f6aaf 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,7 @@ Auto_Jobs_Applier_AIHawk steps in as a game-changing solution to these challenge This file contains sensitive information. Never share or commit this file to version control. -- `llm_api_key: [Your OpenAI or Ollama API key or Gemini API key or Groq API key]` +- `llm_api_key: [Your OpenAI or Ollama API key or Gemini API key or Groq API key or AI/ML API key]` - Replace with your OpenAI API key for GPT integration - To obtain an API key, follow the tutorial at: - 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). @@ -189,6 +189,7 @@ This file contains sensitive information. Never share or commit this file to ver You can find more about your organization limits on the [official page](https://platform.openai.com/settings/organization/limits). - For obtaining Gemini API key visit [Google AI for Devs](https://ai.google.dev/gemini-api/docs/api-key) - For obtaining Groq API key visit [Groq API](https://api.groq.com/v1) + - For obtaining AI/ML API key visite [AI/ML API](https://aimlapi.com/app/) ### 2. work_preferences.yaml @@ -266,7 +267,7 @@ This file defines your job search parameters and bot behavior. Each section cont #### 2.1 config.py - Customize LLM model endpoint - `LLM_MODEL_TYPE`: - - Choose the model type, supported: openai / ollama / claude / gemini / groq + - Choose the model type, supported: openai / ollama / claude / gemini / groq / aiml - `LLM_MODEL`: - Choose the LLM model, currently supported: - openai: gpt-4o @@ -282,6 +283,7 @@ This file defines your job search parameters and bot behavior. Each section cont - claude: - gemini: - groq: + - aiml: - Note: To run local Ollama, follow the guidelines here: [Guide to Ollama deployment](https://github.com/ollama/ollama) ### 3. plain_text_resume.yaml @@ -723,6 +725,8 @@ For further assistance, please create an issue on the [GitHub repository](https: - Written by Rushi, [Linkedin](https://www.linkedin.com/in/rushichaganti/), support him by following. - [OpenAI API Documentation](https://platform.openai.com/docs/) + +- [AI/ML API Documentation](https://docs.aimlapi.com/) ### For Developers From 33c8fbe834f3b7473f5e295d72b5e64b37602692 Mon Sep 17 00:00:00 2001 From: ddondada9 Date: Wed, 20 Nov 2024 21:11:38 -0500 Subject: [PATCH 12/27] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index acd912e0..c8bf8f1a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ langchain==0.2.11 langchain-anthropic langchain-huggingface langchain-community==0.2.10 -langchain-core===0.2.36 +langchain-core==0.2.36 langchain-google-genai==1.0.10 langchain-ollama==0.1.3 langchain-openai==0.1.17 From 3a9fcbb14ea31cd002b345518c9f5dbd1ab0e0d4 Mon Sep 17 00:00:00 2001 From: Akhil Date: Thu, 21 Nov 2024 09:53:39 -0500 Subject: [PATCH 13/27] license update --- LICENSE | 681 ++++++++++++++++++++++++++++++++++++++++++++++++--- README.md | 7 +- docs/LICENSE | 397 ++++++++++++++++++++++++++++++ 3 files changed, 1052 insertions(+), 33 deletions(-) create mode 100644 docs/LICENSE diff --git a/LICENSE b/LICENSE index 492b3047..9aa17f0b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,44 +1,661 @@ -MIT License + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Copyright (c) 2024 AI Hawk & its contibutors + Copyright (C) 2024 AI Hawk FOSS
+ Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + Preamble -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. ---- + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. -“Commons Clause” License Condition v1.0 + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. -The Software is provided to you by the Licensor under the License as defined below, -subject to the following condition. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. -Without limiting other conditions in the License, the grant of rights under -the License will not include, and the License does not grant to you, -the right to Sell the Software. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. -For purposes of this clause “Sell” means practicing any or all of -the rights granted under this license for a fee or other consideration -(including without limitation fees for hosting or consulting/support services related -to using/hosting/deploying this software), a product or service whose value derives, -entirely or substantially from using this software. + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. -Software: Auto Job Applier + The precise terms and conditions for copying, distribution and +modification follow. -License: MIT + TERMS AND CONDITIONS -Licensor: AI Hawk & its contibutors \ No newline at end of file + 1. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md index 62083354..12a31f33 100644 --- a/README.md +++ b/README.md @@ -755,7 +755,12 @@ Made with [contrib.rocks](https://contrib.rocks). ## License -This project is licensed under the MIT + Commons Clause License - see the [LICENSE](LICENSE) file for details. +This project is licensed under the AGPL License. Documentation is licensed under CC BY - see the [AGPL LICENSE](LICENSE) and [CC BY LICENSE](docs/LICENSE) files for details. + +The AGPL License requires that any derivative work must also be open source and distributed under the same license. + +The CC BY License permits others to distribute, remix, adapt, and build upon your work, even for commercial purposes, as long as they credit you for the original creation. + ## Disclaimer diff --git a/docs/LICENSE b/docs/LICENSE new file mode 100644 index 00000000..12429f4b --- /dev/null +++ b/docs/LICENSE @@ -0,0 +1,397 @@ +Copyright (C) 2024 AI Hawk FOSS + +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. \ No newline at end of file From 1f68c277957b89853e3d0f274d676d0bd8df08f5 Mon Sep 17 00:00:00 2001 From: Namami Shanker Date: Thu, 21 Nov 2024 22:33:59 +0530 Subject: [PATCH 14/27] "Log time in Job application result" (cherry picked from commit 6b2371569e22f4e3db3e4bef6c90014ffd224768) --- src/ai_hawk/job_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ai_hawk/job_manager.py b/src/ai_hawk/job_manager.py index a9be82a4..115aee70 100644 --- a/src/ai_hawk/job_manager.py +++ b/src/ai_hawk/job_manager.py @@ -5,6 +5,7 @@ from itertools import product from pathlib import Path from turtle import color +from datetime import datetime from inputimeout import inputimeout, TimeoutOccurred from selenium.common.exceptions import NoSuchElementException @@ -406,7 +407,8 @@ def write_to_file(self, job : Job, file_name, reason=None): "link": job.link, "job_recruiter": job.recruiter_link, "job_location": job.location, - "pdf_path": pdf_path + "pdf_path": pdf_path, + "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } if reason: From a349a4aa7230b75f00e152289f3b45544c027d21 Mon Sep 17 00:00:00 2001 From: Namami Shanker Date: Thu, 21 Nov 2024 23:16:55 +0530 Subject: [PATCH 15/27] "Refactor logging current time" (cherry picked from commit c75f78f038435785e34544168eb252e5238d7aa4) --- src/ai_hawk/job_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ai_hawk/job_manager.py b/src/ai_hawk/job_manager.py index 115aee70..818ef7f5 100644 --- a/src/ai_hawk/job_manager.py +++ b/src/ai_hawk/job_manager.py @@ -401,6 +401,7 @@ def write_to_file(self, job : Job, file_name, reason=None): logger.debug(f"Writing job application result to file: {file_name}") pdf_path = Path(job.resume_path).resolve() pdf_path = pdf_path.as_uri() + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") data = { "company": job.company, "job_title": job.title, @@ -408,7 +409,7 @@ def write_to_file(self, job : Job, file_name, reason=None): "job_recruiter": job.recruiter_link, "job_location": job.location, "pdf_path": pdf_path, - "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "time": current_time } if reason: From 48481b5e951e850b9aac5dae554528333bd796ac Mon Sep 17 00:00:00 2001 From: Timothy Genz <34835862+Tgenz1213@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:09:22 -0600 Subject: [PATCH 16/27] Add files via upload --- .github/pull_request_template.md | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..27f0f78a --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,47 @@ +# Pull Request + +## Overview + +- **Title**: [Descriptive title of the changes] +- **Related Issues**: #[issue number] +- **Type**: + - [ ] Feature + - [ ] Bug Fix + - [ ] Refactor + - [ ] Documentation + - [ ] Other: + +## Description + +[Provide a clear description of the changes and their purpose] + +## Implementation Details + +- [ ] Changes are focused and solve the stated problem +- [ ] Code follows project style guides +- [ ] Complex logic is documented +- [ ] No unnecessary complexity introduced + +## Testing + +- [ ] Unit tests added/updated +- [ ] Integration tests added/updated +- [ ] Manual testing completed +- [ ] All tests passing + +## Documentation & Quality + +- [ ] Project documentation updated +- [ ] Code reviewed for clarity +- [ ] Breaking changes clearly marked +- [ ] Dependencies documented + +## Deployment Impact + +- [ ] Database migrations required? [Yes/No] +- [ ] Configuration changes needed? [Yes/No] +- [ ] Breaking changes? [Yes/No] + +## Additional Notes + +[Add any other context or notes for reviewers] From f21530dc055f8fa5c1f1d449c814654489bc150a Mon Sep 17 00:00:00 2001 From: Timothy Genz <34835862+Tgenz1213@users.noreply.github.com> Date: Thu, 21 Nov 2024 14:26:38 -0600 Subject: [PATCH 17/27] Update pull_request_template.md --- .github/pull_request_template.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 27f0f78a..71d8b1ee 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,8 +1,5 @@ -# Pull Request - -## Overview - - **Title**: [Descriptive title of the changes] +- **Description**: [Provide a clear description of the changes and their purpose] - **Related Issues**: #[issue number] - **Type**: - [ ] Feature @@ -11,10 +8,6 @@ - [ ] Documentation - [ ] Other: -## Description - -[Provide a clear description of the changes and their purpose] - ## Implementation Details - [ ] Changes are focused and solve the stated problem From 04c47cfc4ae860a0b4936d214dd94b799cb23d35 Mon Sep 17 00:00:00 2001 From: Akhil Date: Fri, 22 Nov 2024 01:11:38 -0500 Subject: [PATCH 18/27] Stale bot --- .github/workflows/stale.yml | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000..e2caac67 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,42 @@ +name: Mark and Close Stale Issues + +on: + # Schedule the workflow to run periodically (e.g., daily at 1:30 AM UTC) + schedule: + - cron: "30 1 * * *" + workflow_dispatch: + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - name: Run Stale Action + uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-issue-stale: 10 # Days of inactivity before marking an issue as stale + days-before-issue-close: 5 # Days after being marked stale before closing the issue + stale-issue-label: "stale" # Label to apply to stale issues + exempt-issue-labels: "pinned,important" # Labels to exclude from being marked as stale + exempt-issue-assignees: true # Exempt issues with assignees from being marked as stale + stale-issue-message: "This issue has been marked as stale due to inactivity. Please comment or update if this is still relevant." + close-issue-message: "This issue was closed due to prolonged inactivity." + days-before-pr-stale: 10 # Days of inactivity before marking a PR as stale + days-before-pr-close: 2 # Days after being marked stale before closing the PR + stale-pr-label: "stale" # Label to apply to stale PRs + exempt-pr-labels: "pinned,important" # Labels to exclude from being marked as stale + stale-pr-message: > + "This pull request has been marked as stale due to inactivity. + To keep it open, you can: + - Show progress by updating the PR with new commits. + - Continue the conversation by adding comments or requesting clarification on any blockers. + - Resolve pending feedback by replying to unresolved comments or implementing suggested changes. + - Indicate readiness for review by explicitly requesting a review from maintainers or reviewers. + If no action is taken within 7 days, this pull request will be closed." + close-pr-message: "This PR was closed due to prolonged inactivity." + remove-stale-when-updated: true # Remove the stale label if there is new activity + operations-per-run: 20 # Number of issues to process per run (default is 30) From 86ef242bfafa3bd584431a8395aefb94ba0ebb7c Mon Sep 17 00:00:00 2001 From: Yauheni Muravitski Date: Sat, 23 Nov 2024 23:11:41 +0400 Subject: [PATCH 19/27] fix: :bug: Change selectors due to new html structure LinkedIn has changed their HTML structure therefore app stopped to apply to jobs new --- src/ai_hawk/job_manager.py | 45 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/src/ai_hawk/job_manager.py b/src/ai_hawk/job_manager.py index 818ef7f5..fee4b15b 100644 --- a/src/ai_hawk/job_manager.py +++ b/src/ai_hawk/job_manager.py @@ -253,17 +253,17 @@ def get_jobs_from_page(self): pass try: - job_results = self.driver.find_element(By.CLASS_NAME, "jobs-search-results-list") - browser_utils.scroll_slow(self.driver, job_results) - browser_utils.scroll_slow(self.driver, job_results, step=300, reverse=True) + jobs_container = self.driver.find_element(By.XPATH, '//*[@id="main"]/div/div[2]/div[1]/div') + browser_utils.scroll_slow(self.driver, jobs_container) + browser_utils.scroll_slow(self.driver, jobs_container, step=300, reverse=True) - job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[ - 0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item') - if not job_list_elements: + job_list = self.driver.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') + + if not job_list: logger.debug("No job class elements found on page, skipping.") return [] - return job_list_elements + return job_list except NoSuchElementException: logger.debug("No job results found on the page.") @@ -281,13 +281,14 @@ def read_jobs(self): except NoSuchElementException: pass - job_results = self.driver.find_element(By.CLASS_NAME, "jobs-search-results-list") - browser_utils.scroll_slow(self.driver, job_results) - browser_utils.scroll_slow(self.driver, job_results, step=300, reverse=True) - job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item') - if not job_list_elements: - raise Exception("No job class elements found on page") - job_list = [self.job_tile_to_job(job_element) for job_element in job_list_elements] + jobs_container = self.driver.find_element(By.XPATH, '//*[@id="main"]/div/div[2]/div[1]/div') + browser_utils.scroll_slow(self.driver, jobs_container) + browser_utils.scroll_slow(self.driver, jobs_container, step=300, reverse=True) + + job_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') + if not job_list: + raise Exception("No job elements found on page") + job_list = [self.job_tile_to_job(job_element) for job_element in job_list] for job in job_list: if self.is_blacklisted(job.title, job.company, job.link, job.location): logger.info(f"Blacklisted {job.title} at {job.company} in {job.location}, skipping...") @@ -308,14 +309,14 @@ def apply_jobs(self): except NoSuchElementException: pass - job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[ - 0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item') + jobs_container = self.driver.find_element(By.XPATH, '//*[@id="main"]/div/div[2]/div[1]/div') + job_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') - if not job_list_elements: + if not job_list: logger.debug("No job class elements found on page, skipping") return - job_list = [self.job_tile_to_job(job_element) for job_element in job_list_elements] + job_list = [self.job_tile_to_job(job_element) for job_element in job_list] for job in job_list: @@ -490,9 +491,9 @@ def job_tile_to_job(self, job_tile) -> Job: logger.debug(f"Job link extracted: {job.link}") except NoSuchElementException: logger.warning("Job link is missing.") - + try: - job.company = job_tile.find_element(By.CLASS_NAME, 'job-card-container__primary-description').text + job.company = job_tile.find_element(By.XPATH, './/span[contains(normalize-space(), " · ")]').text.split(' · ')[0].strip() logger.debug(f"Job company extracted: {job.company}") except NoSuchElementException: logger.warning("Job company is missing.") @@ -509,12 +510,12 @@ def job_tile_to_job(self, job_tile) -> Job: logger.warning(f"Failed to extract job ID: {e}", exc_info=True) try: - job.location = job_tile.find_element(By.CLASS_NAME, 'job-card-container__metadata-item').text + job.location = job_tile.find_element(By.XPATH, './/span[contains(normalize-space(), " · ")]').text.split(' · ')[-1].strip() except NoSuchElementException: logger.warning("Job location is missing.") try: - job.apply_method = job_tile.find_element(By.CLASS_NAME, 'job-card-container__apply-method').text + job.apply_method = job_tile.find_element(By.XPATH, ".//div[contains(@class, 'job-card-container__job-insight-text') and normalize-space() = 'Easy Apply']").text except NoSuchElementException: job.apply_method = "Applied" logger.warning("Apply method not found, assuming 'Applied'.") From a2bfb043569fb7bb155fe47c055fead2b7cecca0 Mon Sep 17 00:00:00 2001 From: Yauheni Muravitski Date: Sun, 24 Nov 2024 02:09:29 +0400 Subject: [PATCH 20/27] test: :white_check_mark: Fix test after removing other developer's hack Test had been failing after removing unnecessary wheel ( .find_elements()[0] ) n --- tests/test_aihawk_job_manager.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_aihawk_job_manager.py b/tests/test_aihawk_job_manager.py index cc475059..6dafe4ac 100644 --- a/tests/test_aihawk_job_manager.py +++ b/tests/test_aihawk_job_manager.py @@ -114,25 +114,25 @@ def test_apply_jobs_with_jobs(mocker, job_manager): # Mock no_jobs_element to simulate the absence of "No matching jobs found" banner no_jobs_element = mocker.Mock() no_jobs_element.text = "" # Empty text means "No matching jobs found" is not present - mocker.patch.object(job_manager.driver, 'find_element', - return_value=no_jobs_element) # Mock the page_source to simulate what the page looks like when jobs are present mocker.patch.object(job_manager.driver, 'page_source', return_value="some job content") - # Mock the outer find_elements (scaffold-layout__list-container) + # Mock the outer find_element container_mock = mocker.Mock() # Mock the inner find_elements to return job list items job_element_mock = mocker.Mock() # Simulating two job items - job_elements_list = [job_element_mock, job_element_mock] + job_list = [job_element_mock, job_element_mock] # Return the container mock, which itself returns the job elements list - container_mock.find_elements.return_value = job_elements_list - mocker.patch.object(job_manager.driver, 'find_elements', - return_value=[container_mock]) + container_mock.find_elements.return_value = job_list + mocker.patch.object(job_manager.driver, 'find_element', side_effect=[ + no_jobs_element, + container_mock + ]) job = Job( title="Title", @@ -181,7 +181,8 @@ def test_apply_jobs_with_jobs(mocker, job_manager): job_manager.apply_jobs() # Assertions - assert job_manager.driver.find_elements.call_count == 1 + assert container_mock.find_elements.call_count == 1 + assert job_manager.driver.find_element.call_count == 2 # Called for each job element assert job_manager.job_tile_to_job.call_count == 2 # Called for each job element From e18fcd303d89d14b178c6712180abcdfd5c4fba3 Mon Sep 17 00:00:00 2001 From: Akhil Date: Sun, 24 Nov 2024 15:26:40 -0500 Subject: [PATCH 21/27] fixes [BUG]: Not applying to jobs #919 --- src/ai_hawk/job_manager.py | 43 ++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/ai_hawk/job_manager.py b/src/ai_hawk/job_manager.py index a9be82a4..1e7c3330 100644 --- a/src/ai_hawk/job_manager.py +++ b/src/ai_hawk/job_manager.py @@ -4,6 +4,7 @@ import time from itertools import product from pathlib import Path +import traceback from turtle import color from inputimeout import inputimeout, TimeoutOccurred @@ -13,6 +14,7 @@ from ai_hawk.linkedIn_easy_applier import AIHawkEasyApplier from config import JOB_MAX_APPLICATIONS, JOB_MIN_APPLICATIONS, MINIMUM_WAIT_TIME_IN_SECONDS +import job from src.job import Job from src.logging import logger @@ -155,7 +157,7 @@ def start_applying(self): logger.debug("Starting the application process for this page...") try: - jobs = self.get_jobs_from_page() + jobs = self.get_jobs_from_page(scroll=True) if not jobs: logger.debug("No more jobs found on this page. Exiting loop.") break @@ -166,7 +168,7 @@ def start_applying(self): try: self.apply_jobs() except Exception as e: - logger.error(f"Error during job application: {e}") + logger.error(f"Error during job application: {e} {traceback.format_exc()}") continue logger.debug("Applying to jobs on this page has been completed!") @@ -239,7 +241,7 @@ def start_applying(self): time.sleep(sleep_time) page_sleep += 1 - def get_jobs_from_page(self): + def get_jobs_from_page(self, scroll=False): try: @@ -252,24 +254,30 @@ def get_jobs_from_page(self): pass try: - job_results = self.driver.find_element(By.CLASS_NAME, "jobs-search-results-list") - browser_utils.scroll_slow(self.driver, job_results) - browser_utils.scroll_slow(self.driver, job_results, step=300, reverse=True) + # XPath query to find the ul tag with class scaffold-layout__list-container + job_results_xpath_query = "//ul[contains(@class, 'scaffold-layout__list-container')]" + job_results = self.driver.find_element(By.XPATH, job_results_xpath_query) - job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[ - 0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item') + if scroll: + job_results_scrolableElament = job_results.find_element(By.XPATH,"..") + logger.warning(f'is scrollable: {browser_utils.is_scrollable(job_results_scrolableElament)}') + + browser_utils.scroll_slow(self.driver, job_results_scrolableElament) + browser_utils.scroll_slow(self.driver, job_results_scrolableElament, step=300, reverse=True) + + job_list_elements = job_results.find_elements(By.XPATH, ".//li[contains(@class, 'jobs-search-results__list-item') and contains(@class, 'ember-view')]") if not job_list_elements: logger.debug("No job class elements found on page, skipping.") return [] return job_list_elements - except NoSuchElementException: - logger.debug("No job results found on the page.") + except NoSuchElementException as e: + logger.warning(f'No job results found on the page. \n expection: {traceback.format_exc()}') return [] except Exception as e: - logger.error(f"Error while fetching job elements: {e}") + logger.error(f"Error while fetching job elements: {e} {traceback.format_exc()}") return [] def read_jobs(self): @@ -307,8 +315,7 @@ def apply_jobs(self): except NoSuchElementException: pass - job_list_elements = self.driver.find_elements(By.CLASS_NAME, 'scaffold-layout__list-container')[ - 0].find_elements(By.CLASS_NAME, 'jobs-search-results__list-item') + job_list_elements = self.get_jobs_from_page() if not job_list_elements: logger.debug("No job class elements found on page, skipping") @@ -489,10 +496,10 @@ def job_tile_to_job(self, job_tile) -> Job: logger.warning("Job link is missing.") try: - job.company = job_tile.find_element(By.CLASS_NAME, 'job-card-container__primary-description').text + job.company = job_tile.find_element(By.XPATH, ".//div[contains(@class, 'artdeco-entity-lockup__subtitle')]//span").text logger.debug(f"Job company extracted: {job.company}") - except NoSuchElementException: - logger.warning("Job company is missing.") + except NoSuchElementException as e: + logger.warning(f'Job company is missing. {e} {traceback.format_exc()}') # Extract job ID from job url try: @@ -512,9 +519,9 @@ def job_tile_to_job(self, job_tile) -> Job: try: job.apply_method = job_tile.find_element(By.CLASS_NAME, 'job-card-container__apply-method').text - except NoSuchElementException: + except NoSuchElementException as e: job.apply_method = "Applied" - logger.warning("Apply method not found, assuming 'Applied'.") + logger.warning(f'Apply method not found, assuming \'Applied\'. {e} {traceback.format_exc()}') return job From 2dd187a9a134269ee53846772baadf57117a6bd0 Mon Sep 17 00:00:00 2001 From: Yauheni Muravitski Date: Mon, 25 Nov 2024 01:24:38 +0400 Subject: [PATCH 22/27] fix: Add classes for temporary solution Current solution is fine for hotfix, but not as constant --- src/ai_hawk/job_manager.py | 6 +++--- src/ai_hawk/linkedIn_easy_applier.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ai_hawk/job_manager.py b/src/ai_hawk/job_manager.py index fee4b15b..018233e9 100644 --- a/src/ai_hawk/job_manager.py +++ b/src/ai_hawk/job_manager.py @@ -253,7 +253,7 @@ def get_jobs_from_page(self): pass try: - jobs_container = self.driver.find_element(By.XPATH, '//*[@id="main"]/div/div[2]/div[1]/div') + jobs_container = self.driver.find_element(By.CLASS_NAME, 'scaffold-layout__list-container') browser_utils.scroll_slow(self.driver, jobs_container) browser_utils.scroll_slow(self.driver, jobs_container, step=300, reverse=True) @@ -281,7 +281,7 @@ def read_jobs(self): except NoSuchElementException: pass - jobs_container = self.driver.find_element(By.XPATH, '//*[@id="main"]/div/div[2]/div[1]/div') + jobs_container = self.driver.find_element(By.CLASS_NAME, 'scaffold-layout__list-container') browser_utils.scroll_slow(self.driver, jobs_container) browser_utils.scroll_slow(self.driver, jobs_container, step=300, reverse=True) @@ -309,7 +309,7 @@ def apply_jobs(self): except NoSuchElementException: pass - jobs_container = self.driver.find_element(By.XPATH, '//*[@id="main"]/div/div[2]/div[1]/div') + jobs_container = self.driver.find_element(By.CLASS_NAME, 'scaffold-layout__list-container') job_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') if not job_list: diff --git a/src/ai_hawk/linkedIn_easy_applier.py b/src/ai_hawk/linkedIn_easy_applier.py index f0fea7ab..257b0ee9 100644 --- a/src/ai_hawk/linkedIn_easy_applier.py +++ b/src/ai_hawk/linkedIn_easy_applier.py @@ -376,8 +376,8 @@ def fill_up(self, job_context : JobContext) -> None: EC.presence_of_element_located((By.CLASS_NAME, 'jobs-easy-apply-content')) ) - pb4_elements = easy_apply_content.find_elements(By.CLASS_NAME, 'pb4') - for element in pb4_elements: + input_elements = easy_apply_content.find_elements(By.CLASS_NAME, 'jobs-easy-apply-form-section__grouping') + for element in input_elements: self._process_form_element(element, job_context) except Exception as e: logger.error(f"Failed to find form elements: {e}") From ba3a0879d57a2978adcad517164a2623a789cc8f Mon Sep 17 00:00:00 2001 From: Akhil Date: Sun, 24 Nov 2024 16:38:16 -0500 Subject: [PATCH 23/27] test case fixes --- tests/test_aihawk_job_manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_aihawk_job_manager.py b/tests/test_aihawk_job_manager.py index cc475059..4b46cc0b 100644 --- a/tests/test_aihawk_job_manager.py +++ b/tests/test_aihawk_job_manager.py @@ -129,11 +129,11 @@ def test_apply_jobs_with_jobs(mocker, job_manager): # Simulating two job items job_elements_list = [job_element_mock, job_element_mock] - # Return the container mock, which itself returns the job elements list - container_mock.find_elements.return_value = job_elements_list mocker.patch.object(job_manager.driver, 'find_elements', return_value=[container_mock]) + mocker.patch.object(job_manager, 'get_jobs_from_page', return_value=job_elements_list) + job = Job( title="Title", company="Company", @@ -181,7 +181,7 @@ def test_apply_jobs_with_jobs(mocker, job_manager): job_manager.apply_jobs() # Assertions - assert job_manager.driver.find_elements.call_count == 1 + assert job_manager.get_jobs_from_page.call_count == 1 # Called for each job element assert job_manager.job_tile_to_job.call_count == 2 # Called for each job element From dba7b109a320ddea96425fe926ec6ad055b82b2b Mon Sep 17 00:00:00 2001 From: Yauheni Muravitski Date: Mon, 25 Nov 2024 03:38:58 +0400 Subject: [PATCH 24/27] Rewrite test, merge solution with changes, deduplicate code squash --- src/ai_hawk/job_manager.py | 22 ++---------- tests/test_aihawk_job_manager.py | 60 ++++++++++++++------------------ 2 files changed, 29 insertions(+), 53 deletions(-) diff --git a/src/ai_hawk/job_manager.py b/src/ai_hawk/job_manager.py index 4feb4b81..6225b194 100644 --- a/src/ai_hawk/job_manager.py +++ b/src/ai_hawk/job_manager.py @@ -244,7 +244,6 @@ def start_applying(self): def get_jobs_from_page(self, scroll=False): try: - no_jobs_element = self.driver.find_element(By.CLASS_NAME, 'jobs-search-two-pane__no-results-banner--expand') if 'No matching jobs found' in no_jobs_element.text or 'unfortunately, things aren' in self.driver.page_source.lower(): logger.debug("No matching jobs found on this page, skipping.") @@ -262,11 +261,8 @@ def get_jobs_from_page(self, scroll=False): browser_utils.scroll_slow(self.driver, jobs_container_scrolableElement) browser_utils.scroll_slow(self.driver, jobs_container_scrolableElement, step=300, reverse=True) - jobs_container = self.driver.find_element(By.CLASS_NAME, 'scaffold-layout__list-container') - browser_utils.scroll_slow(self.driver, jobs_container) - browser_utils.scroll_slow(self.driver, jobs_container, step=300, reverse=True) - job_list = self.driver.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') + job_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') if not job_list: logger.debug("No job class elements found on page, skipping.") @@ -310,19 +306,7 @@ def read_jobs(self): continue def apply_jobs(self): - try: - no_jobs_element = self.driver.find_element(By.CLASS_NAME, 'jobs-search-two-pane__no-results-banner--expand') - if 'No matching jobs found' in no_jobs_element.text or 'unfortunately, things aren' in self.driver.page_source.lower(): - logger.debug("No matching jobs found on this page, skipping") - return - except NoSuchElementException: - pass - - job_list_elements = self.get_jobs_from_page() - - if not job_list: - logger.debug("No job class elements found on page, skipping") - return + job_list = self.get_jobs_from_page() job_list = [self.job_tile_to_job(job_element) for job_element in job_list] @@ -521,7 +505,7 @@ def job_tile_to_job(self, job_tile) -> Job: logger.warning("Job location is missing.") try: - job.apply_method = job_tile.find_element(By.CLASS_NAME, 'job-card-container__apply-method').text + job.apply_method = job_tile.find_element(By.XPATH, ".//div[contains(@class, 'job-card-container__job-insight-text') and normalize-space() = 'Easy Apply']").text except NoSuchElementException as e: job.apply_method = "Applied" logger.warning(f'Apply method not found, assuming \'Applied\'. {e} {traceback.format_exc()}') diff --git a/tests/test_aihawk_job_manager.py b/tests/test_aihawk_job_manager.py index 1f7f137c..96eb10e1 100644 --- a/tests/test_aihawk_job_manager.py +++ b/tests/test_aihawk_job_manager.py @@ -71,21 +71,33 @@ def test_get_jobs_from_page_no_jobs(mocker, job_manager): def test_get_jobs_from_page_with_jobs(mocker, job_manager): """Test get_jobs_from_page when job elements are found.""" - # Mock the no_jobs_element to behave correctly - mock_no_jobs_element = mocker.Mock() - mock_no_jobs_element.text = "No matching jobs found" + # Mock no_jobs_element to simulate the absence of "No matching jobs found" banner + no_jobs_element = mocker.Mock() + no_jobs_element.text = "" # Empty text means "No matching jobs found" is not present - # Mocking the find_element to return the mock no_jobs_element - mocker.patch.object(job_manager.driver, 'find_element', - return_value=mock_no_jobs_element) + # Mock the driver to simulate the page source + mocker.patch.object(job_manager.driver, 'page_source', return_value="") - # Mock the page_source - mocker.patch.object(job_manager.driver, 'page_source', - return_value="some page content") + # Mock the outer find_element + container_mock = mocker.Mock() - # Ensure jobs are returned as empty list due to "No matching jobs found" - jobs = job_manager.get_jobs_from_page() - assert jobs == [] # No jobs expected due to "No matching jobs found" + # Mock the inner find_elements to return job list items + job_element_mock = mocker.Mock() + # Simulating two job items + job_elements_list = [job_element_mock, job_element_mock] + + # Return the container mock, which itself returns the job elements list + container_mock.find_elements.return_value = job_elements_list + mocker.patch.object(job_manager.driver, 'find_element', side_effect=[ + no_jobs_element, + container_mock + ]) + + job_manager.get_jobs_from_page() + + assert job_manager.driver.find_element.call_count == 2 + assert container_mock.find_elements.call_count == 1 + def test_apply_jobs_with_no_jobs(mocker, job_manager): @@ -94,9 +106,6 @@ def test_apply_jobs_with_no_jobs(mocker, job_manager): mock_element = mocker.Mock() mock_element.text = "No matching jobs found" - # Mock the driver to simulate the page source - mocker.patch.object(job_manager.driver, 'page_source', return_value="") - # Mock the driver to return the mock element when find_element is called mocker.patch.object(job_manager.driver, 'find_element', return_value=mock_element) @@ -111,28 +120,13 @@ def test_apply_jobs_with_no_jobs(mocker, job_manager): def test_apply_jobs_with_jobs(mocker, job_manager): """Test apply_jobs when jobs are present.""" - # Mock no_jobs_element to simulate the absence of "No matching jobs found" banner - no_jobs_element = mocker.Mock() - no_jobs_element.text = "" # Empty text means "No matching jobs found" is not present - # Mock the page_source to simulate what the page looks like when jobs are present mocker.patch.object(job_manager.driver, 'page_source', return_value="some job content") - # Mock the outer find_element - container_mock = mocker.Mock() - - # Mock the inner find_elements to return job list items + # Simulating two job elements job_element_mock = mocker.Mock() - # Simulating two job items - job_list = [job_element_mock, job_element_mock] - - # Return the container mock, which itself returns the job elements list - container_mock.find_elements.return_value = job_list - mocker.patch.object(job_manager.driver, 'find_element', side_effect=[ - no_jobs_element, - container_mock - ]) + job_elements_list = [job_element_mock, job_element_mock] mocker.patch.object(job_manager, 'get_jobs_from_page', return_value=job_elements_list) @@ -184,8 +178,6 @@ def test_apply_jobs_with_jobs(mocker, job_manager): # Assertions assert job_manager.get_jobs_from_page.call_count == 1 - assert container_mock.find_elements.call_count == 1 - assert job_manager.driver.find_element.call_count == 2 # Called for each job element assert job_manager.job_tile_to_job.call_count == 2 # Called for each job element From 8411cf8c1a321188eed7cd93de326bfe6210b71d Mon Sep 17 00:00:00 2001 From: Yauheni Muravitski Date: Mon, 25 Nov 2024 04:12:28 +0400 Subject: [PATCH 25/27] style: :art: Fix naming back --- src/ai_hawk/job_manager.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ai_hawk/job_manager.py b/src/ai_hawk/job_manager.py index 6225b194..f361c2bb 100644 --- a/src/ai_hawk/job_manager.py +++ b/src/ai_hawk/job_manager.py @@ -262,13 +262,13 @@ def get_jobs_from_page(self, scroll=False): browser_utils.scroll_slow(self.driver, jobs_container_scrolableElement) browser_utils.scroll_slow(self.driver, jobs_container_scrolableElement, step=300, reverse=True) - job_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') + job_element_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') - if not job_list: + if not job_element_list: logger.debug("No job class elements found on page, skipping.") return [] - return job_list + return job_element_list except NoSuchElementException as e: logger.warning(f'No job results found on the page. \n expection: {traceback.format_exc()}') @@ -290,10 +290,10 @@ def read_jobs(self): browser_utils.scroll_slow(self.driver, jobs_container) browser_utils.scroll_slow(self.driver, jobs_container, step=300, reverse=True) - job_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') - if not job_list: + job_element_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') + if not job_element_list: raise Exception("No job elements found on page") - job_list = [self.job_tile_to_job(job_element) for job_element in job_list] + job_list = [self.job_tile_to_job(job_element) for job_element in job_element_list] for job in job_list: if self.is_blacklisted(job.title, job.company, job.link, job.location): logger.info(f"Blacklisted {job.title} at {job.company} in {job.location}, skipping...") @@ -306,9 +306,9 @@ def read_jobs(self): continue def apply_jobs(self): - job_list = self.get_jobs_from_page() + job_element_list = self.get_jobs_from_page() - job_list = [self.job_tile_to_job(job_element) for job_element in job_list] + job_list = [self.job_tile_to_job(job_element) for job_element in job_element_list] for job in job_list: From 22f2c3b28c16e8b5ee23931dd753015b4a1e4549 Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 25 Nov 2024 08:09:36 -0500 Subject: [PATCH 26/27] reveiw changes --- src/ai_hawk/job_manager.py | 36 ++++++++++++++------------------ tests/test_aihawk_job_manager.py | 6 +++--- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/ai_hawk/job_manager.py b/src/ai_hawk/job_manager.py index f361c2bb..45515134 100644 --- a/src/ai_hawk/job_manager.py +++ b/src/ai_hawk/job_manager.py @@ -253,7 +253,9 @@ def get_jobs_from_page(self, scroll=False): pass try: - jobs_container = self.driver.find_element(By.CLASS_NAME, 'scaffold-layout__list-container') + # XPath query to find the ul tag with class scaffold-layout__list-container + jobs_xpath_query = "//ul[contains(@class, 'scaffold-layout__list-container')]" + jobs_container = self.driver.find_element(By.XPATH, jobs_xpath_query) if scroll: jobs_container_scrolableElement = jobs_container.find_element(By.XPATH,"..") @@ -262,7 +264,7 @@ def get_jobs_from_page(self, scroll=False): browser_utils.scroll_slow(self.driver, jobs_container_scrolableElement) browser_utils.scroll_slow(self.driver, jobs_container_scrolableElement, step=300, reverse=True) - job_element_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') + job_element_list = jobs_container.find_elements(By.XPATH, ".//li[contains(@class, 'jobs-search-results__list-item') and contains(@class, 'ember-view')]") if not job_element_list: logger.debug("No job class elements found on page, skipping.") @@ -279,20 +281,8 @@ def get_jobs_from_page(self, scroll=False): return [] def read_jobs(self): - try: - no_jobs_element = self.driver.find_element(By.CLASS_NAME, 'jobs-search-two-pane__no-results-banner--expand') - if 'No matching jobs found' in no_jobs_element.text or 'unfortunately, things aren' in self.driver.page_source.lower(): - raise Exception("No more jobs on this page") - except NoSuchElementException: - pass - - jobs_container = self.driver.find_element(By.CLASS_NAME, 'scaffold-layout__list-container') - browser_utils.scroll_slow(self.driver, jobs_container) - browser_utils.scroll_slow(self.driver, jobs_container, step=300, reverse=True) - job_element_list = jobs_container.find_elements(By.CSS_SELECTOR, 'div[data-job-id]') - if not job_element_list: - raise Exception("No job elements found on page") + job_element_list = self.get_jobs_from_page() job_list = [self.job_tile_to_job(job_element) for job_element in job_element_list] for job in job_list: if self.is_blacklisted(job.title, job.company, job.link, job.location): @@ -483,7 +473,7 @@ def job_tile_to_job(self, job_tile) -> Job: logger.warning("Job link is missing.") try: - job.company = job_tile.find_element(By.XPATH, './/span[contains(normalize-space(), " · ")]').text.split(' · ')[0].strip() + job.company = job_tile.find_element(By.XPATH, ".//div[contains(@class, 'artdeco-entity-lockup__subtitle')]//span").text logger.debug(f"Job company extracted: {job.company}") except NoSuchElementException as e: logger.warning(f'Job company is missing. {e} {traceback.format_exc()}') @@ -500,15 +490,21 @@ def job_tile_to_job(self, job_tile) -> Job: logger.warning(f"Failed to extract job ID: {e}", exc_info=True) try: - job.location = job_tile.find_element(By.XPATH, './/span[contains(normalize-space(), " · ")]').text.split(' · ')[-1].strip() + job.location = job_tile.find_element(By.CLASS_NAME, 'job-card-container__metadata-item').text except NoSuchElementException: logger.warning("Job location is missing.") + try: - job.apply_method = job_tile.find_element(By.XPATH, ".//div[contains(@class, 'job-card-container__job-insight-text') and normalize-space() = 'Easy Apply']").text + job_state = job_tile.find_element(By.XPATH, ".//ul[contains(@class, 'job-card-list__footer-wrapper')]//li[contains(@class, 'job-card-container__apply-method')]").text except NoSuchElementException as e: - job.apply_method = "Applied" - logger.warning(f'Apply method not found, assuming \'Applied\'. {e} {traceback.format_exc()}') + try: + # Fetching state when apply method is not found + job_state = job_tile.find_element(By.XPATH, ".//ul[contains(@class, 'job-card-list__footer-wrapper')]//li[contains(@class, 'job-card-container__footer-job-state')]").text + job.apply_method = "Applied" + logger.warning(f'Apply method not found, state {job_state}. {e} {traceback.format_exc()}') + except NoSuchElementException as e: + logger.warning(f'Apply method and state not found. {e} {traceback.format_exc()}') return job diff --git a/tests/test_aihawk_job_manager.py b/tests/test_aihawk_job_manager.py index 96eb10e1..3335ebff 100644 --- a/tests/test_aihawk_job_manager.py +++ b/tests/test_aihawk_job_manager.py @@ -72,8 +72,8 @@ def test_get_jobs_from_page_no_jobs(mocker, job_manager): def test_get_jobs_from_page_with_jobs(mocker, job_manager): """Test get_jobs_from_page when job elements are found.""" # Mock no_jobs_element to simulate the absence of "No matching jobs found" banner - no_jobs_element = mocker.Mock() - no_jobs_element.text = "" # Empty text means "No matching jobs found" is not present + no_jobs_element_mock = mocker.Mock() + no_jobs_element_mock.text = "" # Empty text means "No matching jobs found" is not present # Mock the driver to simulate the page source mocker.patch.object(job_manager.driver, 'page_source', return_value="") @@ -89,7 +89,7 @@ def test_get_jobs_from_page_with_jobs(mocker, job_manager): # Return the container mock, which itself returns the job elements list container_mock.find_elements.return_value = job_elements_list mocker.patch.object(job_manager.driver, 'find_element', side_effect=[ - no_jobs_element, + no_jobs_element_mock, container_mock ]) From d5295fbbe60098dee8228829f3d9acfc54d0727a Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 25 Nov 2024 08:18:12 -0500 Subject: [PATCH 27/27] review change --- src/ai_hawk/job_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ai_hawk/job_manager.py b/src/ai_hawk/job_manager.py index 45515134..112af685 100644 --- a/src/ai_hawk/job_manager.py +++ b/src/ai_hawk/job_manager.py @@ -14,7 +14,7 @@ from ai_hawk.linkedIn_easy_applier import AIHawkEasyApplier from config import JOB_MAX_APPLICATIONS, JOB_MIN_APPLICATIONS, MINIMUM_WAIT_TIME_IN_SECONDS -import job + from src.job import Job from src.logging import logger