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.
This commit is contained in:
blackms 2024-09-09 17:52:07 +02:00
parent 9ef928569b
commit 7d7110253a
6 changed files with 67 additions and 210 deletions

5
.gitignore vendored
View file

@ -12,4 +12,7 @@ generated_cv*
chrome_profile
answers.json
data*
*virtual
*virtual
data_folder/*.yaml
app_log.log
venv

View file

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

View file

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

View file

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

View file

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

View file

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