new func
This commit is contained in:
parent
35cc5d3bde
commit
b6ceeb44ac
8 changed files with 406 additions and 345 deletions
133
src/gpt.py
133
src/gpt.py
|
|
@ -3,89 +3,28 @@ 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
|
||||
from typing import Dict, List
|
||||
|
||||
import httpx
|
||||
from Levenshtein import distance
|
||||
from dotenv import load_dotenv
|
||||
from httpx import HTTPStatusError
|
||||
from langchain_core.messages.ai import AIMessage
|
||||
from langchain_core.output_parsers import StrOutputParser
|
||||
from langchain_core.prompt_values import StringPromptValue
|
||||
from langchain_core.prompts import ChatPromptTemplate
|
||||
from langchain_openai import ChatOpenAI
|
||||
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: Union[OpenAIModel, OllamaModel, ClaudeModel]):
|
||||
|
||||
def __init__(self, llm: ChatOpenAI):
|
||||
logger.debug("Initializing LLMLogger with LLM: %s", llm)
|
||||
self.llm = llm
|
||||
logger.debug("LLMLogger successfully initialized with LLM: %s", llm)
|
||||
|
|
@ -108,11 +47,10 @@ 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 = {
|
||||
f"prompt_{i+1}": prompt.content
|
||||
f"prompt_{i + 1}": prompt.content
|
||||
for i, prompt in enumerate(prompts.messages)
|
||||
}
|
||||
logger.debug("Prompts converted to dictionary: %s", prompts)
|
||||
|
|
@ -123,7 +61,7 @@ class LLMLogger:
|
|||
logger.debug("Prompts are of unknown type, attempting default conversion")
|
||||
try:
|
||||
prompts = {
|
||||
f"prompt_{i+1}": prompt.content
|
||||
f"prompt_{i + 1}": prompt.content
|
||||
for i, prompt in enumerate(prompts.messages)
|
||||
}
|
||||
logger.debug("Prompts converted to dictionary using default method: %s", prompts)
|
||||
|
|
@ -137,7 +75,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"]
|
||||
|
|
@ -147,14 +85,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
|
||||
|
|
@ -169,7 +107,7 @@ class LLMLogger:
|
|||
"model": model_name,
|
||||
"time": current_time,
|
||||
"prompts": prompts,
|
||||
"replies": parsed_reply["content"], # Response content
|
||||
"replies": parsed_reply["content"],
|
||||
"total_tokens": total_tokens,
|
||||
"input_tokens": input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
|
|
@ -179,7 +117,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)
|
||||
|
|
@ -191,13 +129,12 @@ class LLMLogger:
|
|||
|
||||
|
||||
class LoggerChatModel:
|
||||
def __init__(self, llm: Union[OpenAIModel, OllamaModel, ClaudeModel]):
|
||||
def __init__(self, llm: ChatOpenAI):
|
||||
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:
|
||||
|
|
@ -221,18 +158,25 @@ class LoggerChatModel:
|
|||
|
||||
if retry_after:
|
||||
wait_time = int(retry_after)
|
||||
logger.warning("Rate limit exceeded. Waiting for %d seconds before retrying (extracted from 'retry-after' header)...", wait_time)
|
||||
logger.warning(
|
||||
"Rate limit exceeded. Waiting for %d seconds before retrying (extracted from 'retry-after' header)...",
|
||||
wait_time)
|
||||
time.sleep(wait_time)
|
||||
elif retry_after_ms:
|
||||
wait_time = int(retry_after_ms) / 1000.0
|
||||
logger.warning("Rate limit exceeded. Waiting for %f seconds before retrying (extracted from 'retry-after-ms' header)...", wait_time)
|
||||
logger.warning(
|
||||
"Rate limit exceeded. Waiting for %f seconds before retrying (extracted from 'retry-after-ms' header)...",
|
||||
wait_time)
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
wait_time = 30 # Время ожидания по умолчанию
|
||||
logger.warning("'retry-after' header not found. Waiting for %d seconds before retrying (default)...", wait_time)
|
||||
wait_time = 30
|
||||
logger.warning(
|
||||
"'retry-after' header not found. Waiting for %d seconds before retrying (default)...",
|
||||
wait_time)
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logger.error("HTTP error occurred with status code: %d, waiting 30 seconds before retrying", e.response.status_code)
|
||||
logger.error("HTTP error occurred with status code: %d, waiting 30 seconds before retrying",
|
||||
e.response.status_code)
|
||||
time.sleep(30)
|
||||
|
||||
except Exception as e:
|
||||
|
|
@ -242,8 +186,6 @@ 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:
|
||||
|
|
@ -280,11 +222,11 @@ class LoggerChatModel:
|
|||
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)
|
||||
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)
|
||||
)
|
||||
logger.debug("GPTAnswerer initialized with API key")
|
||||
|
||||
@property
|
||||
|
|
@ -309,7 +251,6 @@ 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)
|
||||
|
||||
|
|
@ -336,14 +277,13 @@ class GPTAnswerer:
|
|||
output = chain.invoke({"text": text})
|
||||
logger.debug("Summary generated: %s", output)
|
||||
return output
|
||||
|
||||
|
||||
def _create_chain(self, template: str):
|
||||
logger.debug("Creating chain with template: %s", template)
|
||||
prompt = ChatPromptTemplate.from_template(template)
|
||||
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),
|
||||
|
|
@ -452,17 +392,14 @@ class GPTAnswerer:
|
|||
chain = prompt | self.llm_cheap | StrOutputParser()
|
||||
output = chain.invoke({"question": question})
|
||||
logger.debug("Section determined from question: %s", output)
|
||||
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(" ", "_")
|
||||
section_name = output.lower().replace(" ", "_")
|
||||
if section_name == "cover_letter":
|
||||
chain = chains.get(section_name)
|
||||
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)
|
||||
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.")
|
||||
|
|
@ -479,7 +416,9 @@ class GPTAnswerer:
|
|||
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({"resume_educations": self.resume.education_details,"resume_jobs": self.resume.experience_details,"resume_projects": self.resume.projects , "question": question})
|
||||
output_str = chain.invoke(
|
||||
{"resume_educations": self.resume.education_details, "resume_jobs": self.resume.experience_details,
|
||||
"resume_projects": self.resume.projects, "question": question})
|
||||
logger.debug("Raw output for numeric question: %s", output_str)
|
||||
try:
|
||||
output = self.extract_number_from_string(output_str)
|
||||
|
|
@ -511,10 +450,12 @@ 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.
|
||||
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'.
|
||||
If the phrase contains 'upload resume', consider it as 'resume'.
|
||||
Do not provide any additional information or explanations.
|
||||
|
||||
phrase: {phrase}
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue