resolve issues

This commit is contained in:
queukat 2024-09-06 20:06:30 +03:00
parent b1de845ec1
commit 35cc5d3bde
3 changed files with 126 additions and 35 deletions

View file

@ -3,6 +3,8 @@ import os
import re
import textwrap
import time
from abc import ABC, abstractmethod
from typing import Dict, List, Union
from datetime import datetime
from functools import wraps
from pathlib import Path
@ -17,15 +19,73 @@ from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompt_values import StringPromptValue
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from Levenshtein import distance
import src.strings as strings
from src.utils import logger
load_dotenv()
class AIModel(ABC):
@abstractmethod
def invoke(self, prompt: str) -> str:
pass
class OpenAIModel(AIModel):
def __init__(self, api_key: str, llm_model: str, llm_api_url: str):
from langchain_openai import ChatOpenAI
self.model = ChatOpenAI(model_name=llm_model, openai_api_key=api_key,
temperature=0.4, base_url=llm_api_url)
def invoke(self, prompt: str) -> str:
print("invoke in openai")
response = self.model.invoke(prompt)
return response
class ClaudeModel(AIModel):
def __init__(self, api_key: str, llm_model: str, llm_api_url: str):
from langchain_anthropic import ChatAnthropic
self.model = ChatAnthropic(model=llm_model, api_key=api_key,
temperature=0.4, base_url=llm_api_url)
def invoke(self, prompt: str) -> str:
response = self.model.invoke(prompt)
return response
class OllamaModel(AIModel):
def __init__(self, api_key: str, llm_model: str, llm_api_url: str):
from langchain_ollama import ChatOllama
self.model = ChatOllama(model=llm_model, base_url=llm_api_url)
def invoke(self, prompt: str) -> str:
response = self.model.invoke(prompt)
return response
class AIAdapter:
def __init__(self, config: dict, api_key: str):
self.model = self._create_model(config, api_key)
def _create_model(self, config: dict, api_key: str) -> AIModel:
llm_model_type = config['llm_model_type']
llm_model = config['llm_model']
llm_api_url = config['llm_api_url']
print('Using {0} with {1} from {2}'.format(llm_model_type, llm_model, llm_api_url))
if llm_model_type == "openai":
return OpenAIModel(api_key, llm_model, llm_api_url)
elif llm_model_type == "claude":
return ClaudeModel(api_key, llm_model, llm_api_url)
elif llm_model_type == "ollama":
return OllamaModel(api_key, llm_model, llm_api_url)
else:
raise ValueError(f"Unsupported model type: {model_type}")
def invoke(self, prompt: str) -> str:
return self.model.invoke(prompt)
class LLMLogger:
def __init__(self, llm: ChatOpenAI):
def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]):
logger.debug("Initializing LLMLogger with LLM: %s", llm)
self.llm = llm
logger.debug("LLMLogger successfully initialized with LLM: %s", llm)
@ -48,6 +108,7 @@ class LLMLogger:
prompts = prompts.text
logger.debug("Prompts converted to text: %s", prompts)
elif isinstance(prompts, Dict):
# Convert prompts to a dictionary if they are not in the expected format
logger.debug("Prompts are of type Dict")
try:
prompts = {
@ -76,7 +137,7 @@ class LLMLogger:
except Exception as e:
logger.error("Error obtaining current time: %s", str(e))
raise
# Extract token usage details from the response
try:
token_usage = parsed_reply["usage_metadata"]
output_tokens = token_usage["output_tokens"]
@ -86,14 +147,14 @@ class LLMLogger:
except KeyError as e:
logger.error("KeyError in parsed_reply structure: %s", str(e))
raise
# Extract model details from the response
try:
model_name = parsed_reply["response_metadata"]["model_name"]
logger.debug("Model name: %s", model_name)
except KeyError as e:
logger.error("KeyError in response_metadata: %s", str(e))
raise
# Calculate the total cost of the API call
try:
prompt_price_per_token = 0.00000015
completion_price_per_token = 0.0000006
@ -108,7 +169,7 @@ class LLMLogger:
"model": model_name,
"time": current_time,
"prompts": prompts,
"replies": parsed_reply["content"], # Контент ответа
"replies": parsed_reply["content"], # Response content
"total_tokens": total_tokens,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
@ -118,7 +179,7 @@ class LLMLogger:
except KeyError as e:
logger.error("Error creating log entry: missing key %s in parsed_reply", str(e))
raise
# Write the log entry to the log file in JSON format
try:
with open(calls_log, "a", encoding="utf-8") as f:
json_string = json.dumps(log_entry, ensure_ascii=False, indent=4)
@ -130,17 +191,18 @@ class LLMLogger:
class LoggerChatModel:
def __init__(self, llm: ChatOpenAI):
def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]):
logger.debug("Initializing LoggerChatModel with LLM: %s", llm)
self.llm = llm
logger.debug("LoggerChatModel successfully initialized with LLM: %s", llm)
def __call__(self, messages: List[Dict[str, str]]) -> str:
# Call the LLM with the provided messages and log the response.
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) # Вызов LLM
reply = self.llm(messages)
logger.debug("LLM response received: %s", reply)
parsed_reply = self.parse_llmresult(reply)
@ -180,6 +242,8 @@ class LoggerChatModel:
continue
def parse_llmresult(self, llmresult: AIMessage) -> Dict[str, Dict]:
# Parse the LLM result into a structured format.
logger.debug("Parsing LLM result: %s", llmresult)
try:
@ -218,10 +282,9 @@ class LoggerChatModel:
class GPTAnswerer:
def __init__(self, openai_api_key):
self.llm_cheap = LoggerChatModel(
ChatOpenAI(model_name="gpt-4o-mini", openai_api_key=openai_api_key, temperature=0.4)
)
def __init__(self, config, llm_api_key):
self.ai_adapter = AIAdapter(config, llm_api_key)
self.llm_cheap = LoggerChatModel(self.ai_adapter)
logger.debug("GPTAnswerer initialized with API key")
@property
@ -246,6 +309,7 @@ class GPTAnswerer:
@staticmethod
def _preprocess_template_string(template: str) -> str:
# Preprocess a template string to remove unnecessary indentation.
logger.debug("Preprocessing template string")
return textwrap.dedent(template)
@ -279,6 +343,7 @@ class GPTAnswerer:
return prompt | self.llm_cheap | StrOutputParser()
def answer_question_textual_wide_range(self, question: str) -> str:
# Define chains for each section of the resume
logger.debug("Answering textual question: %s", question)
chains = {
"personal_information": self._create_chain(strings.personal_information_template),
@ -387,7 +452,11 @@ class GPTAnswerer:
chain = prompt | self.llm_cheap | StrOutputParser()
output = chain.invoke({"question": question})
logger.debug("Section determined from question: %s", output)
section_name = output.lower().replace(" ", "_")
match = re.search(r"(Personal information|Self Identification|Legal Authorization|Work Preferences|Education Details|Experience Details|Projects|Availability|Salary Expectations|Certifications|Languages|Interests|Cover letter)", output, re.IGNORECASE)
if not match:
raise ValueError("Could not extract section name from the response.")
section_name = match.group(1).lower().replace(" ", "_")
if section_name == "cover_letter":
chain = chains.get(section_name)
output = chain.invoke({"resume": self.resume, "job_description": self.job_description})
@ -442,6 +511,7 @@ class GPTAnswerer:
return best_option
def resume_or_cover(self, phrase: str) -> str:
# Define the prompt template
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 the word 'upload', consider it as 'cover'. Do not provide any additional information or explanations.