From 7d7110253ab5bc43855409c81cfdbcdd388e17ce Mon Sep 17 00:00:00 2001 From: blackms Date: Mon, 9 Sep 2024 17:52:07 +0200 Subject: [PATCH] Fix method invocation in LoggerChatModel This commit resolves an issue where the LoggerChatModel class was incorrectly attempting to call instances of AIModel directly as if they were callable objects. Changes include: - Modified __call__ method to explicitly use the invoke method when calling AI models. - Updated constructor documentation to clarify the type of object expected. - Added additional debug logging for better traceability of method entry and exit points. These changes ensure that the LoggerChatModel class aligns with the intended design patterns and correctly utilizes the AIModel instances, improving the maintainability and robustness of the codebase. --- .gitignore | 5 +- data_folder/config.yaml | 52 ------------- data_folder/plain_text_resume.yaml | 119 ----------------------------- data_folder/secrets.yaml | 3 - main.py | 4 +- src/gpt.py | 94 +++++++++++++++-------- 6 files changed, 67 insertions(+), 210 deletions(-) delete mode 100644 data_folder/config.yaml delete mode 100644 data_folder/plain_text_resume.yaml delete mode 100644 data_folder/secrets.yaml diff --git a/.gitignore b/.gitignore index 50bbd27..bd8925b 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,7 @@ generated_cv* chrome_profile answers.json data* -*virtual \ No newline at end of file +*virtual +data_folder/*.yaml +app_log.log +venv \ No newline at end of file diff --git a/data_folder/config.yaml b/data_folder/config.yaml deleted file mode 100644 index 1cbe9ed..0000000 --- a/data_folder/config.yaml +++ /dev/null @@ -1,52 +0,0 @@ -remote: [true/false] - -experienceLevel: - internship: [true/false] - entry: [true/false] - associate: [true/false] - mid-senior level: [true/false] - director: [true/false] - executive: [true/false] - -jobTypes: - full-time: [true/false] - contract: [true/false] - part-time: [true/false] - temporary: [true/false] - internship: [true/false] - other: [true/false] - volunteer: [true/false] - -date: - all time: [true/false] - month: [true/false] - week: [true/false] - 24 hours: [true/false] - -positions: - - position1 - - position2 - -locations: - - Country1 - - Country2 - -applyOnceAtCompany: [true/false] - -distance: 100 - -company_blacklist: - - Company1 - - Company2 - -titleBlacklist: - - word1 - - word2 - -job_applicants_threshold: - min_applicants: 0 - max_applicants: 100 - -llm_model_type: openai -llm_model: gpt-4o -llm_api_url: https://api.pawan.krd/cosmosrp/v1 \ No newline at end of file diff --git a/data_folder/plain_text_resume.yaml b/data_folder/plain_text_resume.yaml deleted file mode 100644 index 82bfd61..0000000 --- a/data_folder/plain_text_resume.yaml +++ /dev/null @@ -1,119 +0,0 @@ -personal_information: - name: "[Your Name]" - surname: "[Your Surname]" - date_of_birth: "[Your Date of Birth]" - country: "[Your Country]" - city: "[Your City]" - address: "[Your Address]" - phone_prefix: "[Your Phone Prefix]" - phone: "[Your Phone Number]" - email: "[Your Email Address]" - github: "[Your GitHub Profile URL]" - linkedin: "[Your LinkedIn Profile URL]" - -education_details: - - education_level: "[Your Education Level]" - institution: "[Your Institution]" - field_of_study: "[Your Field of Study]" - final_evaluation_grade: "[Your Final Evaluation Grade]" - start_date: "[Start Date]" - year_of_completion: "[Year of Completion]" - exam: - exam_name_1: "[Grade]" - exam_name_2: "[Grade]" - exam_name_3: "[Grade]" - exam_name_4: "[Grade]" - exam_name_5: "[Grade]" - exam_name_6: "[Grade]" - -experience_details: - - position: "[Your Position]" - company: "[Company Name]" - employment_period: "[Employment Period]" - location: "[Location]" - industry: "[Industry]" - key_responsibilities: - - responsibility_1: "[Responsibility Description]" - - responsibility_2: "[Responsibility Description]" - - responsibility_3: "[Responsibility Description]" - skills_acquired: - - "[Skill]" - - "[Skill]" - - "[Skill]" - - - position: "[Your Position]" - company: "[Company Name]" - employment_period: "[Employment Period]" - location: "[Location]" - industry: "[Industry]" - key_responsibilities: - - responsibility_1: "[Responsibility Description]" - - responsibility_2: "[Responsibility Description]" - - responsibility_3: "[Responsibility Description]" - skills_acquired: - - "[Skill]" - - "[Skill]" - - "[Skill]" - -projects: - - name: "[Project Name]" - description: "[Project Description]" - link: "[Project Link]" - - - name: "[Project Name]" - description: "[Project Description]" - link: "[Project Link]" - -achievements: - - name: "[Achievement Name]" - description: "[Achievement Description]" - - name: "[Achievement Name]" - description: "[Achievement Description]" - -certifications: - - name: "[Certification Name]" - description: "[Certification Description]" - - name: "[Certification Name]" - description: "[Certification Description]" - -languages: - - language: "[Language]" - proficiency: "[Proficiency Level]" - - language: "[Language]" - proficiency: "[Proficiency Level]" - -interests: - - "[Interest]" - - "[Interest]" - - "[Interest]" - -availability: - notice_period: "[Notice Period]" - -salary_expectations: - salary_range_usd: "[Salary Range]" - -self_identification: - gender: "[Gender]" - pronouns: "[Pronouns]" - veteran: "[Yes/No]" - disability: "[Yes/No]" - ethnicity: "[Ethnicity]" - -legal_authorization: - eu_work_authorization: "[Yes/No]" - us_work_authorization: "[Yes/No]" - requires_us_visa: "[Yes/No]" - requires_us_sponsorship: "[Yes/No]" - requires_eu_visa: "[Yes/No]" - legally_allowed_to_work_in_eu: "[Yes/No]" - legally_allowed_to_work_in_us: "[Yes/No]" - requires_eu_sponsorship: "[Yes/No]" - -work_preferences: - remote_work: "[Yes/No]" - in_person_work: "[Yes/No]" - open_to_relocation: "[Yes/No]" - willing_to_complete_assessments: "[Yes/No]" - willing_to_undergo_drug_tests: "[Yes/No]" - willing_to_undergo_background_checks: "[Yes/No]" diff --git a/data_folder/secrets.yaml b/data_folder/secrets.yaml deleted file mode 100644 index c218803..0000000 --- a/data_folder/secrets.yaml +++ /dev/null @@ -1,3 +0,0 @@ -email: myemaillinkedin@gmail.com -password: ImpossiblePassowrd10 -llm_api_key: 'sk-11KRr4uuTwpRGfeRTfj1T9BlbkFJjP8QTrswHU1yGruru2FR' \ No newline at end of file diff --git a/main.py b/main.py index afa9044..047724b 100644 --- a/main.py +++ b/main.py @@ -9,7 +9,7 @@ from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager from selenium.common.exceptions import WebDriverException, TimeoutException from lib_resume_builder_AIHawk import Resume,StyleManager,FacadeManager,ResumeGenerator -from src.utils import chromeBrowserOptions +from src.utils import chrome_browser_options from src.gpt import GPTAnswerer from src.linkedIn_authenticator import LinkedInAuthenticator from src.linkedIn_bot_facade import LinkedInBotFacade @@ -149,7 +149,7 @@ class FileManager: def init_browser() -> webdriver.Chrome: try: - options = chromeBrowserOptions() + options = chrome_browser_options() service = ChromeService(ChromeDriverManager().install()) return webdriver.Chrome(service=service, options=options) except Exception as e: diff --git a/src/gpt.py b/src/gpt.py index 4107797..3a833fd 100644 --- a/src/gpt.py +++ b/src/gpt.py @@ -70,7 +70,8 @@ class AIAdapter: llm_model_type = config['llm_model_type'] llm_model = config['llm_model'] llm_api_url = config['llm_api_url'] - print('Using {0} with {1} from {2}'.format(llm_model_type, llm_model, llm_api_url)) + print('Using {0} with {1} from {2}'.format( + llm_model_type, llm_model, llm_api_url)) if llm_model_type == "openai": return OpenAIModel(api_key, llm_model, llm_api_url) @@ -79,7 +80,7 @@ class AIAdapter: elif llm_model_type == "ollama": return OllamaModel(api_key, llm_model, llm_api_url) else: - raise ValueError(f"Unsupported model type: {model_type}") + raise ValueError(f"Unsupported model type: {llm_model_type}") def invoke(self, prompt: str) -> str: return self.model.invoke(prompt) @@ -99,7 +100,8 @@ class LLMLogger: logger.debug("Parsed reply received: %s", 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("Logging path determined: %s", calls_log) except Exception as e: logger.error("Error determining the log path: %s", str(e)) @@ -118,18 +120,22 @@ class LLMLogger: } logger.debug("Prompts converted to dictionary: %s", prompts) except Exception as e: - logger.error("Error converting prompts to dictionary: %s", str(e)) + logger.error( + "Error converting prompts to dictionary: %s", str(e)) raise else: - logger.debug("Prompts are of unknown type, attempting default conversion") + logger.debug( + "Prompts are of unknown type, attempting default conversion") try: prompts = { f"prompt_{i + 1}": prompt.content for i, prompt in enumerate(prompts.messages) } - logger.debug("Prompts converted to dictionary using default method: %s", prompts) + logger.debug( + "Prompts converted to dictionary using default method: %s", prompts) except Exception as e: - logger.error("Error converting prompts using default method: %s", str(e)) + logger.error( + "Error converting prompts using default method: %s", str(e)) raise try: @@ -144,7 +150,8 @@ class LLMLogger: output_tokens = token_usage["output_tokens"] input_tokens = token_usage["input_tokens"] total_tokens = token_usage["total_tokens"] - logger.debug("Token usage - Input: %d, Output: %d, Total: %d", input_tokens, output_tokens, total_tokens) + logger.debug("Token usage - Input: %d, Output: %d, Total: %d", + input_tokens, output_tokens, total_tokens) except KeyError as e: logger.error("KeyError in parsed_reply structure: %s", str(e)) raise @@ -159,7 +166,8 @@ class LLMLogger: 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("Total cost calculated: %f", total_cost) except Exception as e: logger.error("Error calculating total cost: %s", str(e)) @@ -178,12 +186,14 @@ class LLMLogger: } logger.debug("Log entry created: %s", log_entry) except KeyError as e: - logger.error("Error creating log entry: missing key %s in parsed_reply", str(e)) + logger.error( + "Error creating log entry: missing key %s in parsed_reply", str(e)) 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("Log entry written to file: %s", calls_log) except Exception as e: @@ -194,23 +204,24 @@ class LLMLogger: class LoggerChatModel: def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]): - self.llm = llm - logger.debug("LoggerChatModel successfully initialized with LLM: %s", llm) + logger.debug( + "LoggerChatModel successfully initialized with LLM: %s", llm) def __call__(self, messages: List[Dict[str, str]]) -> str: - logger.debug("Entering __call__ method with messages: %s", messages) while True: try: logger.debug("Attempting to call the LLM with messages") - reply = self.llm(messages) + # Ensure you're using invoke to call the model + reply = self.llm.invoke(messages) logger.debug("LLM response received: %s", reply) parsed_reply = self.parse_llmresult(reply) logger.debug("Parsed LLM reply: %s", 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 @@ -246,7 +257,8 @@ class LoggerChatModel: except Exception as e: logger.error("Unexpected error occurred: %s", str(e)) - logger.info("Waiting for 30 seconds before retrying due to an unexpected error.") + logger.info( + "Waiting for 30 seconds before retrying due to an unexpected error.") time.sleep(30) continue @@ -279,11 +291,13 @@ class LoggerChatModel: return parsed_result except KeyError as e: - logger.error("KeyError while parsing LLM result: missing key %s", str(e)) + logger.error( + "KeyError while parsing LLM result: missing key %s", str(e)) raise except Exception as e: - logger.error("Unexpected error while parsing LLM result: %s", str(e)) + logger.error( + "Unexpected error while parsing LLM result: %s", str(e)) raise @@ -299,7 +313,8 @@ class GPTAnswerer: @staticmethod def find_best_match(text: str, options: list[str]) -> str: - logger.debug("Finding best match for text: '%s' in options: %s", text, options) + logger.debug( + "Finding best match for text: '%s' in options: %s", text, options) distances = [ (option, distance(text.lower(), option.lower())) for option in options ] @@ -325,10 +340,12 @@ class GPTAnswerer: def set_job(self, job): logger.debug("Setting job: %s", job) self.job = job - self.job.set_summarize_job_description(self.summarize_job_description(self.job.description)) + self.job.set_summarize_job_description( + self.summarize_job_description(self.job.description)) def set_job_application_profile(self, job_application_profile): - logger.debug("Setting job application profile: %s", job_application_profile) + logger.debug("Setting job application profile: %s", + job_application_profile) self.job_application_profile = job_application_profile def summarize_job_description(self, text: str) -> str: @@ -336,7 +353,8 @@ class GPTAnswerer: 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("Summary generated: %s", output) @@ -460,31 +478,37 @@ class GPTAnswerer: r"(Personal information|Self Identification|Legal Authorization|Work Preferences|Education Details|Experience Details|Projects|Availability|Salary Expectations|Certifications|Languages|Interests|Cover letter)", output, re.IGNORECASE) if not match: - raise ValueError("Could not extract section name from the response.") + 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}) + output = chain.invoke( + {"resume": self.resume, "job_description": self.job_description}) logger.debug("Cover letter generated: %s", output) return output resume_section = getattr(self.resume, section_name, None) or getattr(self.job_application_profile, section_name, None) if resume_section is None: - logger.error("Section '%s' not found in either resume or job_application_profile.", section_name) - raise ValueError(f"Section '{section_name}' not found in either resume or job_application_profile.") + logger.error( + "Section '%s' not found in either resume or job_application_profile.", section_name) + 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("Chain not defined for section '%s'", 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("Question answered: %s", output) return output def answer_question_numeric(self, question: str, default_experience: int = 3) -> int: logger.debug("Answering numeric question: %s", question) - func_template = self._preprocess_template_string(strings.numeric_question_template) + func_template = self._preprocess_template_string( + strings.numeric_question_template) prompt = ChatPromptTemplate.from_template(func_template) chain = prompt | self.llm_cheap | StrOutputParser() output_str = chain.invoke( @@ -495,7 +519,8 @@ class GPTAnswerer: output = self.extract_number_from_string(output_str) logger.debug("Extracted number: %d", output) except ValueError: - logger.warning("Failed to extract number, using default experience: %d", default_experience) + logger.warning( + "Failed to extract number, using default experience: %d", default_experience) output = default_experience return output @@ -511,17 +536,20 @@ class GPTAnswerer: def answer_question_from_options(self, question: str, options: list[str]) -> str: logger.debug("Answering question from options: %s", 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}) + output_str = chain.invoke( + {"resume": self.resume, "question": question, "options": options}) logger.debug("Raw output for options question: %s", output_str) best_option = self.find_best_match(output_str, options) logger.debug("Best option determined: %s", best_option) return best_option def resume_or_cover(self, phrase: str) -> str: - logger.debug("Determining if phrase refers to resume or cover letter: %s", phrase) + logger.debug( + "Determining if phrase refers to resume or cover letter: %s", 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'.