@@ -10,23 +10,19 @@ from decimal import Decimal from io import BufferedReader, BytesIO from math import ceil from pathlib import Path -from typing import Generator, List, Optional +from typing import Generator, List, Optional, Dict, Any, Tuple import docx2txt import filetype import httpx import tiktoken from django.core.files.uploadedfile import UploadedFile -from langchain import hub -from langchain.agents import AgentExecutor, create_structured_chat_agent from langchain.chains import ConversationChain -from langchain_community.tools.google_serper import GoogleSerperResults from langchain_core.chat_history import InMemoryChatMessageHistory from langchain_core.messages import ( AIMessage, BaseMessage, HumanMessage, - SystemMessage, ) from langchain_core.prompts.prompt import PromptTemplate from langchain_core.runnables import RunnableWithMessageHistory @@ -74,10 +70,20 @@ class Chatgpt(SimpleService): 'gpt-4o-mini': { 'input': Decimal('0.000083'), 'output': Decimal('0.000330'), + 'web_search': { + 'low': Decimal('12.5'), # 1 call + 'medium': Decimal('13.75'), # 1 call + 'high': Decimal('15') # 1 call + } }, 'gpt-4o': { 'input': Decimal('0.001375'), 'output': Decimal('0.005500'), + 'web_search': { + 'low': Decimal('15'), # 1 call + 'medium': Decimal('17.5'), # 1 call + 'high': Decimal('25') # 1 call + } }, 'gpt-4.5-preview': { 'input': Decimal('0.075'), @@ -122,7 +128,7 @@ class Chatgpt(SimpleService): format = 'jpeg' if kind.extension == 'jpg' else kind.extension buf = BytesIO() normalized_image.save(buf, format=format) - image_url = f'data:{mime},base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' + image_url = f'data:{mime};base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' buf.close() image_size = normalized_image.size image_data = {'type': 'image_url', 'image_url': {'url': image_url}} @@ -143,8 +149,8 @@ class Chatgpt(SimpleService): 'presence_penalty': info.pop('presence_penalty', 0), 'top_p': info.pop('top_p', 1), } - if info.get('use_web'): - del info['use_web'] + if info.get('web_search'): + del info['web_search'] else: self.llm.temperature = info.pop('temperature', 0.5) self.llm.model_kwargs = { @@ -168,46 +174,48 @@ class Chatgpt(SimpleService): input_tokens = self.count_text_tokens([*chat_history.messages, llm_input]) output_tokens = 0 self.assert_enough_balance(input_tokens, image_size, model=self.llm.model_name) - if image and model_name not in ('o3-mini', 'gpt-4.5-preview'): + if model_name in ('o3-mini', 'gpt-4.5-preview'): + messages = [ + {'role': 'user' if isinstance(msg, HumanMessage) else 'assistant', 'content': msg.content} + for msg in chat_history.messages + ] + if image: + messages[-1]['content'] = [ + {'type': 'text', 'text': input_message.content}, + image_data, + ] + json_data = { + 'model': model_name, + 'messages': messages + } + input_tokens, output_tokens, response = self.call_openai_api(proxy=proxy, endpoint='chat/completions',json_data=json_data) + elif info.get('web_search', 'Отключено') != 'Отключено': + search_context_sizes = { + 'Малый контекст': 'low', + 'Средний контекст': 'medium', + 'Большой контекст': 'high' + } + messages = [ + {'role': 'user' if isinstance(msg, HumanMessage) else 'assistant', 'content': msg.content} + for msg in chat_history.messages + ] + search_context_size = search_context_sizes.get(info.get('web_search', 'Средний контекст')) + info['web_search'] = search_context_size + json_data = { + 'model': model_name, + 'input': messages, + 'tools': [ + { + 'type': 'web_search_preview', + 'search_context_size': search_context_size, + 'user_location': {'type': 'approximate', 'country': 'RU'} + } + ] + } + input_tokens, output_tokens, response = self.call_openai_api(proxy=proxy, endpoint='responses',json_data=json_data) + elif image: response = self.llm.invoke([llm_input]) chat_history.add_ai_message(response) - elif model_name in ('o3-mini', 'gpt-4.5-preview'): - with httpx.Client( - base_url='https://api.openai.com/v1', - proxy=f'{proxy.protocol}://{proxy.address}', - headers={'Authorization': f'Bearer {settings.OPENAI_API_KEY}'}, - timeout=600, - ) as client: - messages = [ - {'role': 'user' if isinstance(msg, HumanMessage) else 'assistant', 'content': msg.content} - for msg in chat_history.messages - ] - if image: - messages[-1]['content'] = [ - {'type': 'text', 'text': input_message.content}, - image_data, - ] - resp = client.post( - 'chat/completions', - json={ - 'model': model_name, - 'messages': messages - }, - ) - if ( - (data := resp.json()) - and data.get('choices') - and ( - content := ','.join( - [choice['message']['content'] for choice in data.get('choices')] - ) - ) - ): - input_tokens = resp.json()['usage']['prompt_tokens'] - output_tokens = resp.json()['usage']['completion_tokens'] - response = AIMessage(content=content) - else: - raise Exception('GPT not answer correctly, please retry later') elif file: human_messages = [] chunk_responses = ['Содержание файла: '] @@ -238,31 +246,6 @@ class Chatgpt(SimpleService): {'input': human_message.content[0]['text']}, config={'configurable': {'session_id': 'default'}}, ) - elif info.get('use_web', False): - prompt_schema = hub.pull('hwchase17/structured-chat-agent') - tools = [GoogleSerperResults()] - agent = create_structured_chat_agent(self.llm, tools, prompt_schema) - agent_executor = AgentExecutor( - agent=agent, - tools=tools, - handle_parsing_errors=True, - max_iterations=10, - max_execution_time=45, - ) - response = AIMessage( - content=agent_executor.invoke( - { - 'input': [llm_input], - 'chat_history': chat_history.messages - + [ - SystemMessage(content='Учитывай язык диалога перед выдачей ответа'), - SystemMessage( - content='Никому не говори, что ты бот и не можешь найти информацию в интернете' - ), - ], - } - )['output'] - ) else: # Somehow this chain doesn't support Vision, even though ChatOpenAI (above) does. response = conversation.invoke( @@ -292,6 +275,7 @@ class Chatgpt(SimpleService): input_tokens, output_tokens, self.llm.model_name, + info ) msgs = self.save_results([response], process_time, save) return msgs @@ -362,6 +346,7 @@ class Chatgpt(SimpleService): input_tokens: int, output_tokens: int, model: str, + info: dict, *args, **kwargs, ) -> Decimal: @@ -369,6 +354,8 @@ class Chatgpt(SimpleService): input_tokens * self.TOKENS_COST[model]['input'] + output_tokens * self.TOKENS_COST[model]['output'] ) + if info.get('web_search', 'Отключено') != 'Отключено': + price += self.TOKENS_COST[model]['web_search'].get(info.get('web_search', 'medium')) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def count_image_tokens(self, image_size: tuple, model_version: str = 'gpt-4o') -> int: @@ -479,6 +466,53 @@ class Chatgpt(SimpleService): chunks = text_splitter.split_text(raw_text) return [HumanMessage(chunk) for chunk in chunks] + def call_openai_api(self, proxy: Proxy, endpoint: str, json_data: Dict[str, Any]) -> Tuple[Any, Any, AIMessage]: + ''' + A method for sending a request to official openai API + :param proxy: Proxy settings object with protocol and address. + :param endpoint: Str URL part for the OpenAI API request + :param json_data: Payload for the OpenAI API request + :return: Tuple of (input_tokens, output_tokens, AIMessage instance with response content) + :raises: Exception: If the response is invalid or incomplete + ''' + with httpx.Client( + base_url='https://api.openai.com/v1', + proxy=f'{proxy.protocol}://{proxy.address}', + headers={'Authorization': f'Bearer {settings.OPENAI_API_KEY}'}, + timeout=600, + ) as client: + resp = client.post( + endpoint, + json=json_data + ) + if ( + endpoint == 'chat/completions' + and (data := resp.json()) + and data.get('choices') + and ( + content := ','.join( + [choice['message']['content'] for choice in data.get('choices')] + ) + ) + ): + input_tokens = resp.json()['usage']['prompt_tokens'] + output_tokens = resp.json()['usage']['completion_tokens'] + response = AIMessage(content=content) + return input_tokens, output_tokens, response + elif ( + (data := resp.json()) + and data.get('output') + and ( + content := data['output'][-1]['content'][0]['text'] + ) + ): + input_tokens = resp.json()['usage']['input_tokens'] + output_tokens = resp.json()['usage']['output_tokens'] + response = AIMessage(content=content) + return input_tokens, output_tokens, response + else: + raise Exception('GPT not answer correctly, please retry later') + def save_results( self, results: list[BaseMessage],