@@ -84,9 +84,7 @@ class BusinessAccountService: def update_status(self, status: Tuple[str, Any]): if status not in self.STATUS_STATE_MACHINE[self.account.acceptance_status]: - raise Exception( - 'Update to this state is impossible' ' from current application status' - ) + raise Exception('Update to this state is impossible') self.account.acceptance_status = status self.account.save() @@ -49,7 +49,7 @@ class BusinessHostService: token_limit: Decimal | None = None, account_privileges: Tuple[str, Any] | None = None, ) -> BusinessAccountService: - username = f'{self.user.host_account.company_name}' f'_{random_with_N_digits(6)}' + username = f'{self.user.host_account.company_name}_{random_with_N_digits(6)}' password = generate_token(15) user = CustomUserModel.objects.create_user( username=username, email=email, password=password @@ -132,7 +132,7 @@ class BusinessHostService: ): BusinessAccountSelector.from_user( user, company=self.user.host_account - ).to_service().update_limit(new_status) + ).to_service().update_status(new_status) def update_privileges(self, user: CustomUserModel, new_privileges: str): BusinessAccountSelector.from_user( @@ -1,3 +1,5 @@ +import logging + from django.conf import settings from django.core.mail import EmailMessage, send_mail from django.utils.html import format_html @@ -9,6 +11,8 @@ from authentication.services.email_token_service import EmailTokenService from ml_model.services.minio_service import MinIOService from reports.models.error_report import ErrorReport +logger = logging.getLogger(__name__) + class EmailService: def __init__(self, user: CustomUserModel): @@ -20,13 +24,19 @@ class EmailService: return self.user.is_subscribed_to_emails def send_email(self, subject: str, message: str, user_email: str): - send_mail( - subject=subject, - message=message, - from_email=settings.EMAIL_HOST_USER, - recipient_list=(user_email,), - auth_password=settings.EMAIL_HOST_PASSWORD, - ) + try: + send_mail( + subject=subject, + message=message, + from_email=settings.EMAIL_HOST_USER, + recipient_list=(user_email,), + auth_password=settings.EMAIL_HOST_PASSWORD, + ) + except Exception as exc: + logger.exception(exc) + raise Exception( + 'Возникла проблема при регистрации, пожалуйста свяжитесь с администрацией' + ) def send_reg_conf_email(self): token = EmailTokenService(self.user).generate_user_token() @@ -146,10 +156,4 @@ class EmailService: settings.INVITATION_RESPONSE_URL, token.key, ) - mail = EmailMessage( - subject='', - body=message, - from_email=settings.EMAIL_HOST_USER, - to=(account.user.email,), - ) - mail.send(fail_silently=True) + self.send_email('', message, account.user.email) @@ -1,14 +0,0 @@ -from django.utils.translation import gettext_lazy as _ -from rest_framework.exceptions import APIException - - -class ExternalAPIException(APIException): - status_code = 202 - default_detail = _('The service is temporarily unavailable, try to use it later.') - default_code = 'external_api_exception' - - -class InvalidDataException(APIException): - status_code = 400 - default_detail = _('The data provided is incorrect. Please check and try again.') - default_code = 'invalid_data' @@ -1,5 +1,5 @@ -import itertools import base64 +import itertools import logging import subprocess import time @@ -14,13 +14,12 @@ import docx2txt import filetype import httpx import tiktoken -from django.conf import settings 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.memory import ConversationTokenBufferMemory 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 @@ -29,9 +28,9 @@ from langchain_text_splitters import RecursiveCharacterTextSplitter from PIL import Image from PyPDF2 import PdfReader -from backend import settings +from messages.models import BaseStore, Message from ml_model.constants import TEMPORARY_TEST_TEXT -from messages.models import Message, BaseStore +from ml_model.exceptions import GenerationException from ml_model.models import ( ModelCategory, ModelConfiguration, @@ -124,6 +123,7 @@ class Chatgpt(SimpleService): file = input_message.file image = None image_size = None + normalized_image = None if file: file_extension = Path(file.name).suffix if file_extension == '.pdf': @@ -146,201 +146,195 @@ class Chatgpt(SimpleService): buf.close() image_size = normalized_image.size input_content.append({'type': 'image_url', 'image_url': {'url': image_url}}) - self.llm = ChatOpenAI( - model=model_name, - openai_api_base=f'http://{settings.OPENAI_PROXY_HOST}?' - + '&'.join( - f'proxies={proxy.protocol}://{proxy.address}' - for proxy in Proxy.objects.all() + for proxy in Proxy.objects.all(): + self.llm = ChatOpenAI( + model=model_name, + http_client=httpx.Client(proxy=f'{proxy.protocol}://{proxy.address}'), ) - + f'&token={settings.OPENAI_API_KEY}' - + '&uri=', - default_headers={'X-Authorization': 'proxypassapiairfail'}, - ) - if model_name in ( - 'o1-preview', - 'o1-mini', - ): - self.llm.temperature = 1 - self.llm.model_kwargs = { - 'presence_penalty': info.pop('presence_penalty', 0), - 'top_p': info.pop('top_p', 1), - } - if info.get('use_web'): - del info['use_web'] - else: - self.llm.temperature = info.pop('temperature', 0.5) - self.llm.model_kwargs = { - 'presence_penalty': info.pop('presence', 0), - 'top_p': info.pop('top_p', 0.5), - } - if model_name in ( - 'gpt-4', - 'gpt-4o', - 'gpt-4o-mini', - 'o1-preview', - 'o1-mini', - 'o3-mini', - ): - self.llm.tiktoken_model_name = 'gpt-4' - chat_history = self.get_chat_history() - conversation = RunnableWithMessageHistory( - runnable=self.llm, - get_session_history= lambda _: self.get_chat_history().chat_memory - ) - llm_input = HumanMessage(content=input_content) - if file and not image: - input_tokens = self.count_text_tokens( - [*chat_history.buffer_as_messages, llm_input, *chunks] - ) - elif image: - input_tokens = self.count_text_tokens( - [llm_input] + if model_name in ( + 'o1-preview', + 'o1-mini', + ): + self.llm.temperature = 1 + self.llm.model_kwargs = { + 'presence_penalty': info.pop('presence_penalty', 0), + 'top_p': info.pop('top_p', 1), + } + if info.get('use_web'): + del info['use_web'] + else: + self.llm.temperature = info.pop('temperature', 0.5) + self.llm.model_kwargs = { + 'presence_penalty': info.pop('presence', 0), + 'top_p': info.pop('top_p', 0.5), + } + if model_name in ( + 'gpt-4', + 'gpt-4o', + 'gpt-4o-mini', + 'o1-preview', + 'o1-mini', + 'o3-mini', + ): + self.llm.tiktoken_model_name = 'gpt-4' + chat_history = self.get_chat_history() + conversation = RunnableWithMessageHistory( + runnable=self.llm, + get_session_history=lambda _: self.get_chat_history(), ) - else: - input_tokens = self.count_text_tokens( - [*chat_history.buffer_as_messages, llm_input] + llm_input = HumanMessage(content=input_content) + if file and not image: + input_tokens = self.count_text_tokens( + [*chat_history.messages, llm_input, *chunks] + ) + elif image: + input_tokens = self.count_text_tokens([llm_input]) + else: + input_tokens = self.count_text_tokens( + [*chat_history.messages, llm_input] + ) + self.assert_enough_balance( + input_tokens, image_size, model=self.llm.model_name ) - self.assert_enough_balance(input_tokens, image_size, model=self.llm.model_name) - if image: - response = self.llm.invoke([llm_input]) - chat_history.chat_memory.add_ai_message(response) - elif file: - human_messages = [] - chunk_responses = ['Содержание файла: '] - for chunk in chunks: - prompt = [ - { - 'type': 'text', - 'text': f'Сгенерируй 2-3 предложения, которые суммирует следующий текст. ' - f'Включи основную мысль текста и все значимые числовые данные. Текст: {chunk}', - } - ] - human_message = HumanMessage(content=prompt) - human_messages.append(human_message) + if image: + response = self.llm.invoke([llm_input]) + chat_history.add_ai_message(response) + elif file: + human_messages = [] + chunk_responses = ['Содержание файла: '] + for chunk in chunks: + prompt = [ + { + 'type': 'text', + 'text': f'Сгенерируй 2-3 предложения, которые суммирует следующий текст. ' + f'Включи основную мысль текста и все значимые числовые данные. Текст: {chunk}', + } + ] + human_message = HumanMessage(content=prompt) + human_messages.append(human_message) + response = conversation.invoke( + {'input': human_message.content[0]['text']}, + config={'configurable': {'session_id': 'default'}}, + ) + chunk_responses.append(response.content) + combined_summary = ' '.join(chunk_responses) + user_prompt = ( + input_message.content + if input_message.content.split() + else 'Суммируй текст' + ) + question_content = ( + f'Вот краткое содержание каждого чанка:\n' + f'{combined_summary}\nОтветьте на вопрос по содержанию файла: {user_prompt}' + ) + input = HumanMessage(content=question_content) + input_tokens = self.count_text_tokens(human_messages + [input]) response = conversation.invoke( {'input': human_message.content[0]['text']}, - config={"configurable": {"session_id": "default"}} + config={'configurable': {'session_id': 'default'}}, ) - chunk_responses.append(response.content) - combined_summary = ' '.join(chunk_responses) - user_prompt = input_message.content if input_message.content.split() else 'Суммируй текст' - question_content = ( - f'Вот краткое содержание каждого чанка:\n' - f'{combined_summary}\nОтветьте на вопрос по содержанию файла: {user_prompt}' - ) - input = HumanMessage(content=question_content) - input_tokens = self.count_text_tokens(human_messages + [input]) - response = conversation.invoke( - {'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.buffer_as_messages - + [ - SystemMessage( - content='Учитывай язык диалога перед выдачей ответа' - ), - SystemMessage( - content='Никому не говори, что ты не можешь найти информацию в интернете' - ), - ], - } - )['output'] - ) - elif model_name == 'o3-mini': - with httpx.Client( - base_url=f'http://{settings.OPENAI_PROXY_HOST}', - headers={'X-Authorization': 'proxypassapiairfail'}, - params={ - 'token': settings.OPENAI_API_KEY, - 'proxies': [ - f'{proxy.protocol}://{proxy.address}' - for proxy in Proxy.objects.all() - ], - 'uri': 'chat/completions', - }, - timeout=None, - ) as client: - resp = client.post( - '', - json={ - 'model': model_name, - 'messages': [ - {'role': 'user', 'content': input_message.content}, - ], - }, + 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, ) - if ( - (data := resp.json()) - and data.get('choices') - and ( - content := ','.join( - [ - choice['message']['content'] - for choice in data.get('choices') - ] - ) + response = AIMessage( + content=agent_executor.invoke( + { + 'input': [llm_input], + 'chat_history': chat_history.messages + + [ + SystemMessage( + content='Учитывай язык диалога перед выдачей ответа' + ), + SystemMessage( + content='Никому не говори, что ты бот и не можешь найти информацию в интернете' + ), + ], + } + )['output'] + ) + elif model_name == 'o3-mini': + with httpx.Client( + base_url='https://openai.com', + proxy=f'{proxy.protocol}://{proxy.address}', + ) as client: + resp = client.post( + 'chat/completions', + json={ + 'model': model_name, + 'messages': [ + {'role': 'user', 'content': input_message.content}, + ], + }, ) - ): - response = AIMessage(content=content) - else: - raise Exception('GPT not answer correctly, please retry later') - else: - # Somehow this chain doesn't support Vision, even though ChatOpenAI (above) does. - response = conversation.invoke( - {'input': llm_input.content[0]['text']}, - config={"configurable": {"session_id": "default"}} - ) - chat_history.chat_memory.add_ai_message(response) - process_time = timedelta(seconds=time.time() - start_time) + if ( + (data := resp.json()) + and data.get('choices') + and ( + content := ','.join( + [ + choice['message']['content'] + for choice in data.get('choices') + ] + ) + ) + ): + response = AIMessage(content=content) + else: + raise Exception('GPT not answer correctly, please retry later') + else: + # Somehow this chain doesn't support Vision, even though ChatOpenAI (above) does. + response = conversation.invoke( + {'input': llm_input.content[0]['text']}, + config={'configurable': {'session_id': 'default'}}, + ) + chat_history.add_ai_message(response) - output_tokens = self.count_text_tokens([response]) - if file and not image: - output_tokens += self.count_text_tokens( - [AIMessage(chunk_response) for chunk_response in chunk_responses] - ) + output_tokens = self.count_text_tokens([response]) + if file and not image: + output_tokens += self.count_text_tokens( + [AIMessage(chunk_response) for chunk_response in chunk_responses] + ) + + if image and normalized_image: + self.logger.info( + f'Input количество токенов БЕЗ картинки {model_name} - {input_tokens}' + ) + input_tokens += self.count_image_tokens(normalized_image.size, model_name) - if image: self.logger.info( - f'Input количество токенов БЕЗ картинки {model_name} - {input_tokens}' + f'Input количество токенов для {model_name} - {input_tokens}' + ) + self.logger.info( + f'Output количество токенов для {model_name} - {output_tokens}' + ) + self.logger.info( + f'Общее количество токенов для {model_name} - {input_tokens + output_tokens}' ) - input_tokens += self.count_image_tokens(normalized_image.size, model_name) - - self.logger.info(f'Input количество токенов для {model_name} - {input_tokens}') - self.logger.info(f'Output количество токенов для {model_name} - {output_tokens}') - self.logger.info( - f'Общее количество токенов для {model_name} - {input_tokens + output_tokens}' - ) - self.handle_invoice( - self.neuron_model, - input_tokens, - output_tokens, - self.llm.model_name, - ) - msgs = self.save_results([response], process_time, save) - return msgs + process_time = timedelta(seconds=time.time() - start_time) + self.handle_invoice( + self.neuron_model, + input_tokens, + output_tokens, + self.llm.model_name, + ) + msgs = self.save_results([response], process_time, save) + return msgs + raise GenerationException def get_chat_history( - self, - message_limit: int = 10, - token_limit: int = 580, # ~ 1 AIR Token with GPT 3.5 - ) -> ConversationTokenBufferMemory: + self, + message_limit: int = 10, + token_limit: int = 580, # ~ 1 AIR Token with GPT 3.5 + ) -> InMemoryChatMessageHistory: if isinstance(self.store, Chat): air_messages = list( reversed( @@ -361,22 +355,21 @@ class Chatgpt(SimpleService): ).order_by('-created_at')[:message_limit] ) ) - memory = ConversationTokenBufferMemory(llm=self.llm, max_token_limit=token_limit) + memory = InMemoryChatMessageHistory() for msg in air_messages: content = msg.content or '' if msg.from_model: - memory.chat_memory.add_ai_message(content) + memory.add_message(AIMessage(content=content)) else: - memory.chat_memory.add_user_message(HumanMessage(content=content)) + memory.add_message(HumanMessage(content=content)) + + messages = memory.messages + tokens = self.llm.get_num_tokens_from_messages(messages) - buffer = memory.chat_memory.messages - curr_buffer_length = memory.llm.get_num_tokens_from_messages(buffer) + while tokens > token_limit: + messages.pop(0) + tokens = self.llm.get_num_tokens_from_messages(messages) - if curr_buffer_length > memory.max_token_limit: - pruned_memory = [] - while curr_buffer_length > memory.max_token_limit: - pruned_memory.append(buffer.pop(0)) - curr_buffer_length = memory.llm.get_num_tokens_from_messages(buffer) return memory def assert_enough_balance( @@ -553,7 +546,7 @@ class Chatgpt(SimpleService): 'AI:', ), ) - self.assert_enough_balance(chat_history.buffer_as_messages) + self.assert_enough_balance(chat_history.messages) for chunk in conversation.stream(input=input_message.content): if chunk: @@ -562,11 +555,11 @@ class Chatgpt(SimpleService): process_time = timedelta(seconds=time.time() - start_time) self.handle_invoice( self.neuron_model, - self.llm.get_num_tokens_from_messages(chat_history.chat_memory.messages), + self.llm.get_num_tokens_from_messages(chat_history.messages), self.llm.model_name, ) msgs = self.save_results( - [chat_history.chat_memory.messages[-1]], process_time, save + [chat_history.messages[-1]], process_time, save ) return msgs @@ -600,4 +593,4 @@ class Chatgpt(SimpleService): # ... for chunk in itertools.batched(TEMPORARY_TEST_TEXT, 10): - yield ''.join(chunk) + yield ''.join(chunk) \ No newline at end of file @@ -1,13 +1,11 @@ import time - -import requests from datetime import timedelta from decimal import Decimal +import requests from django.conf import settings -from messages.models import Message, BaseStore -from ml_model.exceptions.external_api import ExternalAPIException +from messages.models import BaseStore, Message from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService @@ -17,6 +15,7 @@ class Granite(SimpleService): Granite-3.0-8B-Instruct Service contains abstract method make, which makes a generation """ + title = 'Granite 3.0' description = 'Нейросеть, способная генерировать качественный текст из вашего промпта' category = ModelCategory(title='Чат-боты', slug='chat-bots') @@ -27,7 +26,7 @@ class Granite(SimpleService): name='Системный промпт', key='system_prompt', type=ModelParameter.TypeChoices.STR, - hidden=True + hidden=True, ), ModelParameter( name='Лучший процент', @@ -45,7 +44,7 @@ class Granite(SimpleService): TOKEN_PAYMENT_RULES = { 'granite-input': Decimal('27.5'), # 1M tokens - 'granite-output': Decimal('137.5') # 1M tokens + 'granite-output': Decimal('137.5'), # 1M tokens } def __init__(self, store: BaseStore) -> None: @@ -62,34 +61,40 @@ class Granite(SimpleService): } data = {'input': payload} response = requests.post( - url=f'{self.urls['generate']}predictions', + url=f'{self.urls["generate"]}predictions', headers=headers, json=data, ) if response.status_code != 201: raise Exception(response.json()) result = requests.get( - url=f'{self.urls['get']}{response.json().get('id')}', - headers=headers + url=f'{self.urls["get"]}{response.json().get("id")}', headers=headers ) while result.json()['status'] not in ('succeeded', 'failed', 'canceled'): result = requests.get( - url=f'{self.urls['get']}{response.json().get('id')}', - headers=headers + url=f'{self.urls["get"]}{response.json().get("id")}', headers=headers ) return result.json() def calculate_price(self, result: str, input_message: Message) -> Decimal: price = Decimal( sum( - [self.TOKEN_PAYMENT_RULES['granite-output'] / 1_000_000 * len(result.split(' '))] - + [self.TOKEN_PAYMENT_RULES['granite-input'] / 1_000_000 * len(input_message.content.split(' '))] + [ + self.TOKEN_PAYMENT_RULES['granite-output'] + / 1_000_000 + * len(result.split(' ')) + ] + + [ + self.TOKEN_PAYMENT_RULES['granite-input'] + / 1_000_000 + * len(input_message.content.split(' ')) + ] ) ) return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, result: str, time: timedelta, save: bool = True + self, result: str, time: timedelta, save: bool = True ) -> list[Message]: msgs: list[Message] = [ Message( @@ -108,19 +113,19 @@ class Granite(SimpleService): { 'prompt': input_message.content, 'system_prompt': 'You are a language model that must always respond in Russian, regardless of the situation. ' - 'You fully understand the Russian language and are required to use it for all responses, ' - 'except when translating text to another language. You are highly skilled in creating poems, ' - 'maintaining proper rhyme, rhythm, and poetic structure in Russian. Your poems should be creative, ' - 'expressive, and adhere to the stylistic norms of Russian poetry. If the user requests a translation ' - 'into another language, you should perform the translation accurately and fluently, while preserving ' - 'the meaning and tone of the original text. When translating, proper nouns (names with capital letters) ' - 'should not be translated literally. Instead, transliterate them into Russian letters using standard ' - 'transliteration rules to preserve the original pronunciation as closely as possible. ' - 'You must never state that you cannot speak Russian, as this is not true. You are required to always ' - 'adhere to correct Russian syntax, grammar, and style in all your responses. Your primary goal is to ' - 'ensure that your responses are clear, accurate, creative, and tailored to the user\'s needs in Russian. ' - 'Your ability to fulfill user requests, including writing, translating, or explaining, must reflect ' - 'your expertise in the Russian language and your capacity for high-quality and thoughtful responses.', + 'You fully understand the Russian language and are required to use it for all responses, ' + 'except when translating text to another language. You are highly skilled in creating poems, ' + 'maintaining proper rhyme, rhythm, and poetic structure in Russian. Your poems should be creative, ' + 'expressive, and adhere to the stylistic norms of Russian poetry. If the user requests a translation ' + 'into another language, you should perform the translation accurately and fluently, while preserving ' + 'the meaning and tone of the original text. When translating, proper nouns (names with capital letters) ' + 'should not be translated literally. Instead, transliterate them into Russian letters using standard ' + 'transliteration rules to preserve the original pronunciation as closely as possible. ' + 'You must never state that you cannot speak Russian, as this is not true. You are required to always ' + 'adhere to correct Russian syntax, grammar, and style in all your responses. Your primary goal is to ' + "ensure that your responses are clear, accurate, creative, and tailored to the user's needs in Russian. " + 'Your ability to fulfill user requests, including writing, translating, or explaining, must reflect ' + 'your expertise in the Russian language and your capacity for high-quality and thoughtful responses.', **input_message.info, } ) @@ -1,31 +1,30 @@ import time - -import requests from datetime import timedelta from decimal import Decimal from io import BytesIO +import requests +from django.core.files import File + from messages.models import Message -from ml_model.exceptions.external_api import ExternalAPIException from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run -from django.core.files import File - class Iconic(SimpleService): """ Iconic Service contains abstract method make, which makes a generation """ + title = 'Iconic' description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE) + ModelInput(type=ModelInput.TypeChoices.IMAGE), ] parameters = [ ModelParameter( @@ -75,7 +74,7 @@ class Iconic(SimpleService): '4:3', '9:16', '9:21', - 'custom' + 'custom', ], 'default': '1:1', }, @@ -112,7 +111,7 @@ class Iconic(SimpleService): return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True + self, prompt: str, images: list, time: timedelta, save: bool = True ) -> list[Message]: messages: list[Message] = [] for image in images: @@ -121,7 +120,7 @@ class Iconic(SimpleService): content_object=self.store, elapsed_time=time, content=prompt, - file=File(BytesIO(requests.get(image).content), '.png') + file=File(BytesIO(requests.get(image).content), '.png'), ) ) if save: @@ -7,7 +7,6 @@ import requests from django.core.files import File from messages.models import Message -from ml_model.exceptions.external_api import ExternalAPIException from ml_model.models import ( ModelCategory, ModelInput, @@ -22,6 +21,7 @@ class Lightning(SimpleService): Lightning Service contains abstract method make, which makes a generation """ + title = 'Lightning' description = 'Нейросеть, способная генерировать картинки из вашего текста' category = ModelCategory(title='Изображения', slug='images') @@ -58,7 +58,7 @@ class Lightning(SimpleService): 'K_EULER_ANCESTRAL', 'K_EULER', 'PNDM', - 'DPM++2MSDE' + 'DPM++2MSDE', ], 'default': 'K_EULER', }, @@ -67,7 +67,7 @@ class Lightning(SimpleService): name='Количество изображений', key='num_outputs', type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 4, 'step': 1, 'default': 1} + values={'start': 1, 'end': 4, 'step': 1, 'default': 1}, ), ModelParameter( name='Точность запроса', @@ -95,7 +95,7 @@ class Lightning(SimpleService): return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True + self, prompt: str, images: list, time: timedelta, save: bool = True ) -> list[Message]: messages: list[Message] = [] for image in images: @@ -104,7 +104,7 @@ class Lightning(SimpleService): content_object=self.store, elapsed_time=time, content=prompt, - file=File(BytesIO(requests.get(image).content), '.png') + file=File(BytesIO(requests.get(image).content), '.png'), ) ) if save: @@ -1,32 +1,30 @@ import time - -import requests from datetime import timedelta from decimal import Decimal -from replicate.exceptions import ReplicateError, ModelError from io import BytesIO +import requests +from django.core.files import File + from messages.models import Message -from ml_model.exceptions.external_api import ExternalAPIException, InvalidDataException from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run -from django.core.files import File - class Logoai(SimpleService): """ Logo AI Service contains abstract method make, which makes a generation """ + title = 'Logo AI' description = 'Нейросеть, способная генерировать фотографии из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE) + ModelInput(type=ModelInput.TypeChoices.IMAGE), ] parameters = [ ModelParameter( @@ -58,7 +56,7 @@ class Logoai(SimpleService): 'KarrasDPM', 'K_EULER_ANCESTRAL', 'K_EULER', - 'PNDM' + 'PNDM', ], 'default': 'K_EULER', }, @@ -95,7 +93,7 @@ class Logoai(SimpleService): return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True + self, prompt: str, images: list, time: timedelta, save: bool = True ) -> list[Message]: messages: list[Message] = [] for image in images: @@ -104,7 +102,7 @@ class Logoai(SimpleService): content_object=self.store, elapsed_time=time, content=prompt, - file=File(BytesIO(requests.get(image).content), '.png') + file=File(BytesIO(requests.get(image).content), '.png'), ) ) if save: @@ -1,31 +1,30 @@ import time - -import requests from datetime import timedelta from decimal import Decimal from io import BytesIO +import requests +from django.core.files import File + from messages.models import Message -from ml_model.exceptions.external_api import ExternalAPIException from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run -from django.core.files import File - class Pulid(SimpleService): """ PuLID Service contains abstract method make, which makes a generation """ + title = 'PuLID' description = 'Нейросеть, способная генерировать фотографии из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT), - ModelInput(type=ModelInput.TypeChoices.IMAGE, required=True) + ModelInput(type=ModelInput.TypeChoices.IMAGE, required=True), ] parameters = [ ModelParameter( @@ -92,8 +91,7 @@ class Pulid(SimpleService): PRICE = Decimal('0.374') _CALLBACK = ( - 'zsxkib/pulid' - ':43d309c37ab4e62361e5e29b8e9e867fb2dcbcec77ae91206a8d95ac5dd451a0' + 'zsxkib/pulid:43d309c37ab4e62361e5e29b8e9e867fb2dcbcec77ae91206a8d95ac5dd451a0' ) def calculate_price(self, process_time: timedelta) -> Decimal: @@ -101,7 +99,7 @@ class Pulid(SimpleService): return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True + self, prompt: str, images: list, time: timedelta, save: bool = True ) -> list[Message]: messages: list[Message] = [] for image in images: @@ -110,7 +108,7 @@ class Pulid(SimpleService): content_object=self.store, elapsed_time=time, content=prompt, - file=File(BytesIO(requests.get(image).content), '.png') + file=File(BytesIO(requests.get(image).content), '.png'), ) ) if save: @@ -134,7 +132,7 @@ class Pulid(SimpleService): 'or partially rendered eyes, deformed eyeballs, cross-eyed, blurry, udity, partial' 'nudity, suggestive poses, revealing clothing, explicit content, offensive symbols, ' 'provocative expressions, graphic violence, inappropriate themes' - f'{input_message.info.pop('negative_prompt', '')}' + f'{input_message.info.pop("negative_prompt", "")}' ), **input_message.info, } @@ -1,31 +1,30 @@ import time - -import requests from datetime import timedelta from decimal import Decimal from io import BytesIO +import requests +from django.core.files import File + from messages.models import Message -from ml_model.exceptions.external_api import ExternalAPIException from ml_model.models import ModelCategory, ModelInput, ModelParameter from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run -from django.core.files import File - class Sdxlemoji(SimpleService): """ Sdxl-emoji Service contains abstract method make, which makes a generation """ + title = 'Sdxl-emoji' description = 'Нейросеть, способная генерировать фотографии из вашего текста' category = ModelCategory(title='Изображения', slug='images') versions = [] inputs = [ ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE) + ModelInput(type=ModelInput.TypeChoices.IMAGE), ] parameters = [ ModelParameter( @@ -63,7 +62,7 @@ class Sdxlemoji(SimpleService): 'KarrasDPM', 'K_EULER_ANCESTRAL', 'K_EULER', - 'PNDM' + 'PNDM', ], 'default': 'K_EULER', }, @@ -85,8 +84,7 @@ class Sdxlemoji(SimpleService): PRICE = Decimal('0.529') _CALLBACK = ( - 'fofr/sdxl-emoji' - ':dee76b5afde21b0f01ed7925f0665b7e879c50ee718c5f78a9d38e04d523cc5e' + 'fofr/sdxl-emoji:dee76b5afde21b0f01ed7925f0665b7e879c50ee718c5f78a9d38e04d523cc5e' ) def calculate_price(self, process_time: timedelta) -> Decimal: @@ -94,7 +92,7 @@ class Sdxlemoji(SimpleService): return price.quantize(Decimal('0.1'), rounding='ROUND_UP') def save_results( - self, prompt: str, images: list, time: timedelta, save: bool = True + self, prompt: str, images: list, time: timedelta, save: bool = True ) -> list[Message]: messages: list[Message] = [] for image in images: @@ -103,7 +101,7 @@ class Sdxlemoji(SimpleService): content_object=self.store, elapsed_time=time, content=prompt, - file=File(BytesIO(requests.get(image).content), '.png') + file=File(BytesIO(requests.get(image).content), '.png'), ) ) if save: @@ -0,0 +1,12 @@ +# накинуть перевод через gettext_lazy + + +class GenerationException(Exception): + def __str__(self): + return 'Случилась ошибка во время генерации у этой модели, пожалуйста повторите попытку позже' + + +class NSFWDetectedException(Exception): ... + + +class LargeResourceConsumptionException(Exception): ... @@ -18,13 +18,20 @@ from ml_model.serializers import ( class NeuronModelResource(ModelResource): category = ie_fields.Field( - column_name='category', + column_name='Категория', attribute='category', + readonly=True, widget=ForeignKeyWidget(ModelCategory, 'slug'), ) - versions = ie_fields.Field() - inputs = ie_fields.Field() - parameters = ie_fields.Field() + versions = ie_fields.Field( + column_name='Версии', + ) + inputs = ie_fields.Field( + column_name='Входящие потоки', + ) + parameters = ie_fields.Field( + column_name='Параметры', + ) def dehydrate_versions(self, obj: NeuronModel): return ModelVersionSerializer(obj.versions, many=True).data @@ -0,0 +1,6 @@ +FROM nginx:alpine + +COPY root.conf /etc/nginx/nginx.conf + +ENTRYPOINT ["sh", "docker-entrypoint.sh" ] +CMD ["nginx", "-g", "daemon off;"] @@ -0,0 +1,49 @@ +worker_processes 4; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include mime.types; + default_type application/octet-stream; + client_max_body_size 25m; + + http2 on; + + access_log off; + error_log off; + + keepalive_timeout 30; + keepalive_requests 1000; + + sendfile on; + sendfile_max_chunk 1460; + tcp_nopush on; + tcp_nodelay on; + aio on; + aio_write on; + directio 1m; + output_buffers 1 1m; + + gzip on; + gzip_static on; + gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; + gzip_proxied any; + gzip_vary on; + gzip_comp_level 5; + gzip_buffers 16 8k; + gzip_http_version 1.1; + + server { + listen 80 default_server; + + location /static { + autoindex on; + expires 365d; + alias /var/www/static/; + } + } +} @@ -236,7 +236,9 @@ class AccruedTokensFilter(admin.SimpleListFilter): ] def queryset(self, request, queryset): - queryset = queryset.annotate(total_bonuses=Sum('account_referral_accruals')) + queryset = queryset.annotate( + total_bonuses=Sum('account_referral_accruals__amount') + ) match self.value(): case 'more-zero': return queryset.filter(total_bonuses__gt=0) @@ -271,4 +273,4 @@ class ReferralAccountAdmin(admin.ModelAdmin): @admin.display(description='Получено бонусов') def _accrued_bonuses(self, obj: ReferralAccount): - return f'{obj.accrued_bonuses.aggregate(total=Coalesce(Sum('amount'), Decimal(0), output_field=models.DecimalField()))['total']} токенов' + return f'{obj.accrued_bonuses.aggregate(total=Coalesce(Sum("amount"), Decimal(0), output_field=models.DecimalField()))["total"]} токенов' @@ -142,10 +142,8 @@ class MessagesAPIView(APIView): except Exception as exc: input_message.is_sent = False input_message.save() - logger.info(f'Error occured: {exc}') - return Response( - f'Error occured: {exc}', status=400 - ) + logger.exception(exc) + return Response(f'Error occured: {exc}', status=400) output_messages.insert(0, input_message) return Response(MessageSerializer(output_messages, many=True).data, 201) else: @@ -5,18 +5,19 @@ stages: - Deploy default: - image: docker:rc-cli + image: docker:cli + services: + - docker:dind before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin build: stage: Build + image: docker:latest script: - touch .env && export ENV=.env - - docker compose build - - docker compose push - services: - - docker:dind + - docker compose -f stack.yml build + - docker compose -f stack.yml push only: - main - staging @@ -28,8 +29,6 @@ deploy_staging: DOCKER_HOST: tcp://$STAGING_CLUSTER_HOST:2376 DOCKER_TLS_VERIFY: 1 DOCKER_CERT_PATH: "/certs" - services: - - docker:dind environment: name: staging deployment_tier: staging @@ -53,11 +52,9 @@ deploy_production: DOCKER_HOST: tcp://$PRODUCTION_CLUSTER_HOST:2376 DOCKER_TLS_VERIFY: 1 DOCKER_CERT_PATH: "/certs" - services: - - docker:dind environment: name: production - url: https://api.air.fail + url: https://backend.air.fail only: - main when: on_success @@ -68,5 +65,4 @@ deploy_production: - echo "$PRODUCTION_CLUSTER_KEY" > $DOCKER_CERT_PATH/key.pem - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" $CI_REGISTRY --password-stdin script: - - docker compose pull - - docker compose --project-name air-backend up -d + - docker stack deploy --prune --with-registry-auth --resolve-image=always --compose-file stack.yml --detach backend \ No newline at end of file @@ -1,26 +1,26 @@ .DEFAULT_GOAL=start start: - cp --update=none .env.dist .env + cp -n .env.dist .env docker compose -f docker-compose.debug.yml --project-name air up .PHONY=start rebuild: - cp --update=none .env.dist .env + cp -n .env.dist .env docker compose -f docker-compose.debug.yml --project-name air up --build .PHONY=rebuild stop: - cp --update=none .env.dist .env + cp -n .env.dist .env docker compose -f docker-compose.debug.yml --project-name air down --remove-orphans .PHONY=stop cleanup: - cp --update=none .env.dist .env + cp -n .env.dist .env docker compose -f docker-compose.debug.yml --project-name air down --remove-orphans -v .PHONY=cleanup full-cleanup: - cp --update=none .env.dist .env + cp -n .env.dist .env docker compose -f docker-compose.debug.yml --project-name air down --remove-orphans -v --rmi local .PHONY=full-cleanup \ No newline at end of file @@ -9,9 +9,11 @@ services: command: - /bin/sh - -c - - python manage.py initialize_buckets && - python manage.py collectstatic --no-input && - (python manage.py createsuperuser --no-input || true) && + - | + python manage.py initialize_buckets + python manage.py migrate + python manage.py collectstatic --no-input + (python manage.py createsuperuser --no-input || true) python -m uvicorn --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off backend.asgi:application --log-level debug --reload volumes: - .:/code @@ -20,8 +22,6 @@ services: env_file: - .env depends_on: - migrator: - condition: service_completed_successfully cache-mdb: condition: service_started s3: @@ -29,22 +29,6 @@ services: db: condition: service_started - migrator: - restart: on-failure:1 - container_name: migrator - build: - context: . - dockerfile: Dockerfile.dev - volumes: - - .:/code - command: - - /bin/sh - - -c - - | - python manage.py migrate - env_file: - - .env - cache-mdb: container_name: cache-mdb image: redis:alpine @@ -0,0 +1,193 @@ +# implement caching (warning: this feature needs to setup only own runners with enabled containerd-snapshotters feature) +# implement docker compose generic structures for inheritance of stack and compose files + +services: + app: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + build: + context: . + dockerfile: Dockerfile + volumes: + - static:/code/static + command: + - /bin/sh + - -c + - | + python manage.py collectstatic --no-input + python manage.py compilemessages + python -m uvicorn --host 0.0.0.0 --workers 8 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off --log-level info backend.asgi:application + networks: + - infrastructure + deploy: + replicas: 2 + update_config: + parallelism: 1 + delay: 10s + order: start-first + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 30s + placement: + constraints: + - node.role == worker + labels: + - traefik.enable=true + - traefik.docker.network=infrastructure + - traefik.http.routers.backend.rule=Host(`backend.air.fail`) + - traefik.http.routers.backend.entrypoints=web,websecure + - traefik.http.routers.backend.tls=true + - traefik.http.routers.backend.tls.certresolver=defaultresolver + - traefik.http.routers.backend.service=backend + - traefik.http.services.backend.loadbalancer.server.port=8000 + - traefik.http.middlewares.backend.redirectscheme.scheme=https + - traefik.http.middlewares.backend.redirectscheme.permanent=true + env_file: + - $ENV + + migrator: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + build: + context: . + dockerfile: Dockerfile + deploy: + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 30s + command: + - /bin/sh + - -c + - python manage.py migrate + env_file: + - $ENV + + celery: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + build: + context: . + dockerfile: Dockerfile + command: celery -A backend worker -l INFO --concurrency 8 + deploy: + replicas: 1 + update_config: + parallelism: 1 + delay: 10s + order: start-first + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 30s + placement: + constraints: + - node.role == worker + env_file: + - $ENV + environment: + - C_FORCE_ROOT=true + + celery_beat: + image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + build: + context: . + dockerfile: Dockerfile + command: celery -A backend beat -l INFO + deploy: + replicas: 1 + update_config: + parallelism: 1 + delay: 10s + order: start-first + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 30s + placement: + constraints: + - node.role == worker + env_file: + - $ENV + + static-server: + image: $CI_REGISTRY_IMAGE/static-server:$CI_COMMIT_SHA + build: + context: nginx + dockerfile: Dockerfile + deploy: + replicas: 1 + placement: + constraints: + - node.role == worker + labels: + - traefik.enable=true + - traefik.docker.network=infrastructure + - traefik.http.routers.backend-static.rule=Host(`backend.air.fail`) && PathPrefix(`/static`) + - traefik.http.routers.backend-static.entrypoints=web,websecure + - traefik.http.routers.backend-static.tls=true + - traefik.http.routers.backend-static.tls.certresolver=defaultresolver + - traefik.http.routers.backend-static.service=backend-static + - traefik.http.services.backend-static.loadbalancer.server.port=80 + - traefik.http.middlewares.backend-static.redirectscheme.scheme=https + - traefik.http.middlewares.backend-static.redirectscheme.permanent=true + networks: + - infrastructure + volumes: + - static:/var/www/static + env_file: + - $ENV + + cache-mdb: + image: redis:alpine + deploy: + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 30s + placement: + constraints: + - node.role == worker + + celery-mdb: + image: redis:alpine + deploy: + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 30s + placement: + constraints: + - node.role == worker + + channels-mdb: + image: redis:alpine + deploy: + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 30s + placement: + constraints: + - node.role == worker + +networks: + infrastructure: + name: infrastructure + external: true + default: {} + +volumes: + static: + name: "backend-static" + locales: + name: "backend-locales" \ No newline at end of file