@@ -8,7 +8,7 @@ "connect": { "port": 5678 }, - "justMyCode": false, + "justMyCode": true, "pathMappings": [ { "localRoot": "${workspaceFolder}", @@ -51,43 +51,48 @@ class CustomUserModelManager(BaseUserManager): class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): FIRST_NAME_PLACEHOLDERS = [ - 'Очаровательный', - 'Смышлёный', - 'Забавный', + 'Любопытный', + 'Позитивный', + 'Веселый', + 'Искренний', + 'Смелый', + 'Игривый', + 'Добрый', 'Дружелюбный', 'Шустрый', - 'Талантливый', - 'Кокетливый', - 'Поэтичный', - 'Храбрый', - 'Добродушный', - 'Загадочный', + 'Милый', + 'Лучезарный', + 'Умный', ] LAST_NAME_PLACEHOLDERS = [ - 'Филин', - 'Жираф', - 'Лев', + 'Цыпленок', + 'Кот', + 'Щенок', + 'Хомяк', + 'Кролик', + 'Ежик', + 'Лис', 'Медведь', + 'Бельчонок', 'Пингвин', - 'Ягнёнок', - 'Пони', - 'Муравей', - 'Карп', - 'Василиск', + 'Осьминог', + 'Бобер', ] LAST_NAME_AVATARS = { - 'Филин': 'owl.png', - 'Жираф': 'giraffe.png', - 'Лев': 'lion.png', + 'Цыпленок': 'chicken.png', + 'Кот': 'cat.png', + 'Щенок': 'puppy.png', + 'Хомяк': 'hamster.png', + 'Кролик': 'rabbit.png', + 'Ежик': 'hedgehog.png', + 'Лис': 'fox.png', 'Медведь': 'bear.png', + 'Бельчонок': 'squirrel.png', 'Пингвин': 'penguin.png', - 'Ягнёнок': 'lamb.png', - 'Пони': 'horse.png', - 'Муравей': 'ant.png', - 'Карп': 'fish.png', - 'Василиск': 'lizard.png', + 'Осьминог': 'octopus.png', + 'Бобер': 'beaver.png', } def random_first_name(*args, **kwargs): @@ -185,6 +190,7 @@ class CustomUserModel(AbstractBaseUser, PermissionsMixin, BaseModel): for prefix in ('googleusercontent', 'yandex') ): return self.profile_picture_name + return MinIOService().get_object_link('air-profiles', self.profile_picture_name) @property @@ -2,7 +2,7 @@ import logging from django.conf import settings from django.core.mail import EmailMessage, send_mail -from django.utils.html import format_html +from django.utils.html import format_html, strip_tags from django.utils.translation import gettext_lazy as _ from authentication.models import BusinessAccount, BusinessUserHost @@ -27,7 +27,8 @@ class EmailService: try: send_mail( subject=subject, - message=message, + message=strip_tags(message), + html_message=message, from_email=settings.EMAIL_HOST_USER, recipient_list=(user_email,), auth_password=settings.EMAIL_HOST_PASSWORD, @@ -208,6 +208,7 @@ DATABASES = { 'PASSWORD': env.str('POSTGRES_PASSWORD'), 'HOST': env.str('POSTGRES_HOST'), 'PORT': env.str('POSTGRES_PORT'), + 'CONN_HEALTH_CHECKS': True, } } @@ -0,0 +1,17 @@ +# Generated by Django 5.0.11 on 2025-03-17 07:46 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('ml_model', '0043_merge_0042_alter_modelinput_type_0042_modelstat'), + ] + + operations = [ + migrations.AlterModelOptions( + name='modelstat', + options={'ordering': ('-created_at',), 'verbose_name': 'Статистика по модели', 'verbose_name_plural': 'Статистики по моделям'}, + ), + ] @@ -13,10 +13,11 @@ from ml_model.services.kandinsky import Kandinsky from ml_model.services.lightning import Lightning from ml_model.services.llama import Llama from ml_model.services.logoai import Logoai +from ml_model.services.midjourney import Midjourney from ml_model.services.mistral import Mistral from ml_model.services.musicgen import Musicgen -from ml_model.services.openjourney import Openjourney from ml_model.services.pulid import Pulid +from ml_model.services.qwen import Qwen from ml_model.services.recraft import Recraft from ml_model.services.sdxlemoji import Sdxlemoji from ml_model.services.stablediffusion import Stablediffusion @@ -18,8 +18,8 @@ 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 @@ -47,6 +47,8 @@ from tools.copywrite.models import Copywrite from tools.public_api.models import APIStore +from backend import settings + class Chatgpt(SimpleService): """ ChatGPT Service @@ -138,8 +140,9 @@ class Chatgpt(SimpleService): kind = filetype.guess(input_message.file.read(20)) mime = kind.mime if kind else 'application/octet-stream' normalized_image = Image.open(image) + format = 'jpeg' if kind.extension == 'jpg' else kind.extension buf = BytesIO() - normalized_image.save(buf, format=kind.extension.upper()) + normalized_image.save(buf, format=format) image_url = ( f'data:{mime},base64,{base64.b64encode(buf.getvalue()).decode("utf-8")}' ) @@ -180,25 +183,25 @@ class Chatgpt(SimpleService): chat_history = self.get_chat_history() conversation = RunnableWithMessageHistory( runnable=self.llm, - get_session_history=lambda _: self.get_chat_history().chat_memory, + get_session_history=lambda _: self.get_chat_history(), ) 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] + [*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.buffer_as_messages, llm_input] + [*chat_history.messages, llm_input] ) 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) + chat_history.add_ai_message(response) elif file: human_messages = [] chunk_responses = ['Содержание файла: '] @@ -248,7 +251,7 @@ class Chatgpt(SimpleService): content=agent_executor.invoke( { 'input': [llm_input], - 'chat_history': chat_history.buffer_as_messages + 'chat_history': chat_history.messages + [ SystemMessage( content='Учитывай язык диалога перед выдачей ответа' @@ -262,8 +265,10 @@ class Chatgpt(SimpleService): ) elif model_name == 'o3-mini': with httpx.Client( - base_url='https://openai.com', + 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( 'chat/completions', @@ -295,7 +300,7 @@ class Chatgpt(SimpleService): {'input': llm_input.content[0]['text']}, config={'configurable': {'session_id': 'default'}}, ) - chat_history.chat_memory.add_ai_message(response) + chat_history.add_ai_message(response) output_tokens = self.count_text_tokens([response]) if file and not image: @@ -331,10 +336,10 @@ class Chatgpt(SimpleService): 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( @@ -355,22 +360,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)) - buffer = memory.chat_memory.messages - curr_buffer_length = memory.llm.get_num_tokens_from_messages(buffer) + messages = memory.messages + tokens = self.llm.get_num_tokens_from_messages(messages) + + 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( @@ -430,15 +434,15 @@ class Chatgpt(SimpleService): encoding = tiktoken.get_encoding('o200k_base') total_tokens = 0 for message in messages: - total_tokens += len( - encoding.encode( - message.content - if isinstance(message.content, str) - else ''.join( - [input_data.get('text', '') for input_data in message.content] - ) + if isinstance(message.content, str): + total_tokens += len(encoding.encode(message.content)) + elif any(isinstance(item, dict) for item in message.content): + total_tokens += len(encoding.encode( + ''.join([input_data.get('text', '') for input_data in message.content])) ) - ) + else: + total_tokens += len(encoding.encode(''.join(message.content))) + return total_tokens def get_pdf_data(self, pdf_file: UploadedFile) -> str: @@ -547,7 +551,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: @@ -556,11 +560,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 @@ -139,26 +139,26 @@ class Flux(SimpleService): versions[0].slug: ModelPaymentRule( strategy=ModelPaymentRule.StrategyChoices.FIXED, interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=0.33, + cost=0.3, coefficient=5.00, ), versions[1].slug: ModelPaymentRule( strategy=ModelPaymentRule.StrategyChoices.FIXED, interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=4.4, - coefficient=5.00, + cost=4.00, + coefficient=2.00, ), versions[2].slug: ModelPaymentRule( strategy=ModelPaymentRule.StrategyChoices.FIXED, interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=2.75, - coefficient=5.00, + cost=2.5, + coefficient=2.00, ), versions[3].slug: ModelPaymentRule( strategy=ModelPaymentRule.StrategyChoices.FIXED, interaction_type=ModelPaymentRule.InteractionTypeChoices.INPUT, - cost=6.6, - coefficient=5.00, + cost=6.00, + coefficient=2.00, ), } @@ -12,9 +12,9 @@ from ml_model.services.base import SimpleService from ml_model.tasks import replicate_run -class Openjourney(SimpleService): +class Midjourney(SimpleService): """ - Openjourney Service + Midjourney Service contains abstract method make, which makes a generation """ @@ -29,16 +29,7 @@ class Openjourney(SimpleService): key='aspect_ratio', type=ModelParameter.TypeChoices.LIST, values={ - 'availables': [ - "1:1", - "16:9", - "4:3", - "3:2", - "2:3", - "3:4", - "9:16", - "21:9" - ], + 'availables': ['1:1', '16:9', '4:3', '3:2', '2:3', '3:4', '9:16', '21:9'], 'default': '1:1', }, ), @@ -89,6 +80,8 @@ class Openjourney(SimpleService): callback_data = dict(prompt=activation_prompt, **input_message.info) results = replicate_run(self._CALLBACK, callback_data) process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, input_message=input_message) + self.handle_invoice( + input_message.content_object.model, input_message=input_message + ) msgs = self.save_results(input_message.content, results, process_time, save) return msgs @@ -0,0 +1,113 @@ +import time +from datetime import timedelta +from decimal import Decimal +from typing import Iterator, Any + +from messages.models import Message +from ml_model.models import ModelCategory, ModelVersion, ModelInput, ModelParameter +from ml_model.services.base import SimpleService +from ml_model.tasks import openrouter_run +from tools.chats.models import Chat +from tools.copywrite.models import Copywrite +from tools.public_api.models import APIStore + + +class Qwen(SimpleService): + """ + Qwen Service + contains abstract method make, which makes a generation + """ + + title = 'Qwen' + description = 'Нейросеть, способная генерировать еще больше текста из вашего текста' + category = ModelCategory(title='Чат-боты', slug='chat-bots') + versions = [ + ModelVersion(name='QwQ 32B', default=True, slug='qwq-32b'), + # ModelVersion(name='QwQ 32B', slug='qwq-32b:free'), + ] + inputs = [ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), ] + parameters = [] + TOKENS_COST = { + 'qwq-32b': {'input': Decimal('24'), 'output': Decimal('36')}, # 1M tokens + 'qwq-32b:free': {'input': Decimal('0'), 'output': Decimal('0')}, # 1M tokens + } + + def calculate_price( + self, version: str, input_tokens: int, output_tokens: int + ) -> Decimal: + price_map = self.TOKENS_COST[version.split('/')[1]] + price = ( + input_tokens * price_map['input'] / 1_000_000 + + output_tokens * price_map['output'] / 1_000_000 + ) + return price.quantize(Decimal('0.1'), rounding='ROUND_UP') + + def save_results( + self, content: Iterator[Any], t: timedelta, save: bool = True + ) -> list[Message]: + msgs = [ + Message( + content=content, + content_object=self.store, + elapsed_time=t, + ) + ] + if save: + return Message.objects.bulk_create(msgs) + return msgs + + def make(self, input_message: Message, save: bool = True) -> list[Message]: + start_time = time.time() + version = f'qwen/{input_message.info.pop('version', 'qwq-32b')}' + callback_data = { + 'provider': { + 'order': ['DeepInfra'] + }, + **input_message.info + } + messages = self.get_chat_history() + result = openrouter_run(version, messages, callback_data, self.title) + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice( + input_message.content_object.model, + version=version, + input_tokens=result[1], + output_tokens=result[2] + ) + msgs = self.save_results(result[0], process_time) + return msgs + + def get_chat_history(self, message_limit: int = 10, max_character_limit: int = 1500): + if isinstance(self.store, Chat): + air_messages = list( + reversed( + Message.objects.filter( + chats_chats_messages=self.store, is_deleted=False, is_sent=True + ).order_by('-created_at')[:message_limit] + ) + ) + elif isinstance(self.store, APIStore): + air_messages = [] + elif isinstance(self.store, Copywrite): + air_messages = list( + reversed( + Message.objects.filter( + copywrite_copywrites_messages=self.store, + is_deleted=False, + is_sent=True, + ).order_by('-created_at')[:message_limit] + ) + ) + memory = [] + for msg in air_messages: + content = msg.content or '' + if msg.from_model: + memory.append({"role": "assistant", "content": content}) + else: + memory.append({"role": "user", "content": content}) + character_length = sum(len(content['content']) for content in memory) + while character_length > max_character_limit: + memory.pop(0) + character_length = sum(len(content['content']) for content in memory) + + return memory @@ -1,16 +1,19 @@ +import logging import time import uuid from datetime import timedelta from decimal import Decimal from io import BytesIO +import httpx +from django.conf import settings from django.core.files import File -from backend import settings from messages.models import Message -from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion from ml_model.services.base import SimpleService -from ml_model.tasks import create_new_sd_image, create_sd_image +from poller.models import Proxy + +logger = logging.getLogger(__name__) class Stablediffusion(SimpleService): @@ -18,203 +21,76 @@ class Stablediffusion(SimpleService): Stablediffusion Service contains abstract method make, which makes a generation """ - title = 'StableDiffusion' - description = 'Нейросеть, способная генерировать картинки из вашего текста' - category = ModelCategory(title='Изображения', slug='images') - versions = [ - ModelVersion(name='V3', slug='sd3', default=True), - ModelVersion(name='V3-Turbo', slug='sd3-turbo'), - ModelVersion(name='Standart', slug='stable-diffusion-xl-1024-v1-0'), - ] - inputs = [ - ModelInput(type=ModelInput.TypeChoices.TEXT, required=True), - ModelInput(type=ModelInput.TypeChoices.IMAGE), - ] - parameters = [ - ModelParameter( - name='Sampler', - key='sampler', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': ['DDIM', 'K_EULER', 'K_EULER_ANCESTRAL', 'K_DMP_2'], - 'default': 'DDIM', - }, - ), - ModelParameter( - name='Шаги предобработки', - key='num_inference_steps', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 100, 'step': 1, 'default': 10}, - ), - ModelParameter( - name='Шаги процесса диффузии', - key='steps', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 50, 'step': 1, 'default': 30}, - ), - ModelParameter( - name='Количество изображений', - key='num_images', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 10, 'step': 1, 'default': 1}, - ), - ModelParameter( - name='CFG Scale', - key='cfg_scale', - type=ModelParameter.TypeChoices.INTRANGE, - values={'start': 1, 'end': 10, 'step': 1, 'default': 1}, - ), - ModelParameter( - name='Стиль', - key='style', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': [ - '3d-model', - 'analog-film', - 'anime', - 'cinematic', - 'comic-book', - 'digital-art', - 'enhance', - 'fantasy-art', - 'isometric', - 'line-art', - 'low-poly', - 'modeling-compound', - 'neon-punk', - 'origami', - 'photographic', - 'pixel-art', - 'tile-texture', - ], - 'default': 'photographic', - }, - ), - ModelParameter( - name='Clip Guidance Scale', - key='clip_guidance_scale', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': ['FAST_BLUE', 'FAST_GREEN', 'SIMPLE', 'SLOW', 'SLOWEST'], - 'default': 'SIMPLE', - }, - ), - ModelParameter( - name='Соотношение сторон', - key='aspect_ratio', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': - [ - '1:1', - '16:9', - '9:16', - ], - 'default': '1:1' - }, - ), - ModelParameter( - name='Соотношение сторон', - key='aspect_ratio', - type=ModelParameter.TypeChoices.LIST, - values={ - 'availables': - [ - '1:1', - '4:3', - '3:4', - '3:2', - '16:9', - '9:16', - '24:10', - '10:24' - ], - 'default': '1:1' - }, - ), - ] - - TOKEN_PRICE = Decimal('11') - _API_KEY = settings.STABLE_DIFFUSION_API_KEY + MODELS = ['sd3', 'sd3-turbo'] + MODELS_LINKS = { + 'sd3': 'stable-diffusion-3.5-large', + 'sd3-turbo': 'stable-diffusion-3.5-large-turbo', + } def calculate_price(self, input_message: Message) -> Decimal: - if ( - input_message.info.get('version') or input_message.info.get('engine', 'sd3') - ) == 'sd3': - return Decimal('71.5') - elif ( - input_message.info.get('version') or input_message.info.get('engine', 'sd3') - ) == 'sd3-turbo': - return Decimal('44') - steps = input_message.info.get('steps', 30) - default_price = Decimal('0.9') * self.TOKEN_PRICE - return default_price if steps <= 30 else default_price * Decimal((steps / 30)) + if input_message.info.get('version') == 'sd3': + return Decimal('13') + elif input_message.info.get('version') == 'sd3-turbo': + return Decimal('8') def save_results( - self, input_prompt: str, r: list[BytesIO], t: timedelta, save: bool = True + self, + input_prompt: str, + link: str, + t: timedelta, + save: bool = True, ) -> list[Message]: out: list[Message] = [] - for obj in r: - out.append( - Message( - content_object=self.store, - elapsed_time=t, - content=input_prompt, - file=File( - obj, - f'{uuid.uuid4()}.png', - ), - ) + out.append( + Message( + content_object=self.store, + elapsed_time=t, + content=input_prompt, + file=File( + BytesIO(httpx.get(link).content), + f'{uuid.uuid4()}.png', + ), ) + ) if save: return Message.objects.bulk_create(out) return out def make(self, input_message: Message, save: bool = True) -> list[Message]: - aspect_rations = { - '1:1': (1024, 1024), - '4:3': (1152, 896), - '3:4': (896, 1152), - '3:2': (1216, 832), - '16:9': (1344, 768), - '9:16': (768, 1344), - '24:10': (1536, 640), - '10:24': (640, 1536) - } start_time = time.time() info = input_message.info.copy() - model_name = info.get('version') or info.get('engine', 'sd3') - formatter = { - 'width': int( - aspect_rations[input_message.info.get('aspect_ratio', '1:1')][0] - ), - 'height': int( - aspect_rations[input_message.info.get('aspect_ratio', '1:1')][1] - ), - } + model_name = self.MODELS_LINKS[info.get('version', 'sd3')] translated_prompt = self.translate_prompt(input_message.content) - callback_data = dict( - prompt=translated_prompt, - ) - if input_message.file: - callback_data.update({'init_image': BytesIO(input_message.file.read())}) - if style := input_message.info.get('style', None): - callback_data.update({'style_preset': style}) - if model_name in ('sd3', 'sd3-turbo'): - callback_data = dict( - prompt=translated_prompt, - model=model_name, - aspect_ratio=input_message.info.get('aspect_ratio', '1:1'), - output_format='jpeg', - ) - results = create_new_sd_image.delay(callback_data) - else: - callback_data.update(**formatter, engine=model_name) - results = create_sd_image.delay(self._API_KEY, callback_data) - images = results.get() - process_time = timedelta(seconds=(time.time() - start_time)) - self.handle_invoice(input_message.content_object.model, input_message) - msgs = self.save_results(input_message.content, images, process_time, save) - return msgs + callback_data = { + 'prompt': translated_prompt, + 'aspect_ratio': input_message.info.get('aspect_ratio', '1:1'), + 'output_quality': 100, + 'output_format': 'png', + } + link = '' + for proxy in Proxy.objects.all(): + with httpx.Client( + headers={ + 'Authorization': f'Bearer {settings.REPLICATE_API_KEY}', + 'Prefer': 'wait', + 'Content-Type': 'application/json', + }, + timeout=600, + proxy=f'{proxy.protocol}://{proxy.address}', + ) as client: + result = client.post( + f'https://api.replicate.com/v1/models/stability-ai/{model_name}/predictions', + json={'input': callback_data}, + ) + while result.json()['status'] not in ('succeeded', 'failed', 'canceled'): + result = client.get(result.json()['urls']['get']) + if result.json()['status'] in ('failed', 'canceled'): + logger.error(result.json()['logs']) + raise Exception('No answer from Stable Diffusion, please retry later') + link = result.json()['output'][0] + + process_time = timedelta(seconds=(time.time() - start_time)) + self.handle_invoice(input_message.content_object.model, input_message) + msgs = self.save_results(input_message.content, link, process_time, save) + return msgs @@ -94,7 +94,7 @@ class NeuronModel(BaseModel, OrderedModel): @property def blocked(self) -> bool: - return bool(self.settings) and self.settings.is_active and self.inputs.count() > 0 + return self.settings and not self.settings.is_active def __str__(self): return self.title @@ -6,6 +6,7 @@ from io import BytesIO from typing import IO, Any, Dict import deepl +import httpx import replicate import requests from celery import shared_task @@ -112,6 +113,37 @@ def replicate_run(callback_url: str, payload: dict[str, Any]): ) +@shared_task +def openrouter_run(version: str, messages: list, callback_data: dict, model_name: str): + with httpx.Client( + base_url='https://openrouter.ai/api/v1', + headers={'Authorization': f'Bearer {settings.OPENROUTER_API_KEY}'}, + ) as client: + resp = client.post( + 'chat/completions', + json={ + 'model': version, + 'messages': messages, + **callback_data + }, + ) + if ( + (data := resp.json()) + and data.get('choices') + and ( + content := ','.join( + [choice['message']['content'] for choice in data.get('choices')] + ) + ) + ): + return ( + content, + data['usage']['prompt_tokens'], + data['usage']['completion_tokens'] + ) + raise Exception(f'No answer from {model_name}, please retry later') + + @shared_task def upscale_run(payload: dict[str, tuple[str, IO]]) -> list[str]: content = requests.post( @@ -2,6 +2,7 @@ from decimal import Decimal from django.utils.translation import gettext_lazy as _ +import math class InsufficientBalance(Exception): def __init__( @@ -11,11 +12,13 @@ class InsufficientBalance(Exception): ): self.account_balance = account_balance self.requested_amount = requested_amount + self.needed = requested_amount - account_balance + result_message = _( - 'Token balance: %(balance)d,\nRequired amount: %(required)d,\nNeeded %(needed)d more' + 'Token balance: %(balance).2f,\nRequired amount: %(required)s,\nNeeded %(needed)s more' ) % { 'balance': self.account_balance, - 'required': self.requested_amount, - 'needed': self.requested_amount - self.account_balance, + 'required': math.ceil(self.requested_amount) if self.requested_amount % 1 == 0 else f'~{math.ceil(self.requested_amount)}', + 'needed': round(self.needed,2) if self.needed >= 1 else f'~{math.ceil(self.needed)}' } super().__init__(result_message) @@ -0,0 +1,20 @@ +# Generated by Django 5.0.11 on 2025-03-17 07:46 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payments', '0014_auto_20241116_1547'), + ] + + operations = [ + migrations.AddField( + model_name='paymentplan', + name='points', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(), default=[], help_text='Перечислять через запятую', size=None, verbose_name='Поинты'), + preserve_default=False, + ), + ] @@ -0,0 +1,19 @@ +# Generated by Django 5.0.11 on 2025-03-17 08:04 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payments', '0015_paymentplan_points'), + ] + + operations = [ + migrations.AlterField( + model_name='paymentplan', + name='points', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(), blank=True, help_text='Перечислять через запятую', null=True, size=None, verbose_name='Поинты'), + ), + ] @@ -0,0 +1,19 @@ +# Generated by Django 5.0.11 on 2025-03-17 08:15 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payments', '0016_alter_paymentplan_points'), + ] + + operations = [ + migrations.AlterField( + model_name='paymentplan', + name='points', + field=django.contrib.postgres.fields.ArrayField(base_field=models.CharField(), blank=True, default=list, help_text='Перечислять через запятую', size=None, verbose_name='Поинты'), + ), + ] @@ -2,6 +2,7 @@ from datetime import datetime from dateutil.relativedelta import relativedelta from django.contrib.auth import get_user_model +from django.contrib.postgres.fields import ArrayField from django.db import models from django.utils.translation import gettext_lazy as _ from django_celery_beat.models import PeriodicTask @@ -33,13 +34,20 @@ class PaymentPlan(BaseModel): default=MONTH, ) is_visible = models.BooleanField(default=True, verbose_name=_('Is visible')) + points = ArrayField( + default=list, + blank=True, + base_field=models.CharField(), + verbose_name='Поинты', + help_text='Перечислять через запятую', + ) accessed_models = models.ManyToManyField( NeuronModel, verbose_name='Доступные модели', ) def __str__(self) -> str: - return f"{self.title or 'Ошибка'}" + return f'{self.title or "Ошибка"}' class Meta: ordering = ['price'] @@ -83,7 +91,7 @@ class PaymentPlanUserInfo(BaseModel): return super().save(force_insert, force_update, using, update_fields) def __str__(self) -> str: - return f"{self.user.email or 'Ошибка'}" + return f'{self.user.email or "Ошибка"}' class Meta: verbose_name = _('User Balance') @@ -12,6 +12,7 @@ class PaymentPlanSerializer(serializers.Serializer): price = serializers.DecimalField(max_digits=10, decimal_places=2) tokens_per_plan = serializers.DecimalField(max_digits=50, decimal_places=2) duration = serializers.CharField(read_only=True) + points = serializers.ListField(read_only=True) accessed_models = serializers.SlugRelatedField( slug_field='slug', queryset=NeuronModel.objects.all(), many=True ) @@ -3,6 +3,7 @@ SECRET_KEY=testtest DEBUG=true # NEURON MODELS +OPENROUTER_API_KEY=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 OPENAI_API_KEY=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 STABLE_DIFFUSION_API_KEY=sk-fztQxZobaL0SD7PgpmK7XMQlyNivpKFZNJnqAVG2CcbvAP6Z REPLICATE_API_KEY=r8_HBk6Ts5UJU60nDOUl1V6Uej4ihAAxUc3HAZLO @@ -11,8 +11,8 @@ services: - -c - | python manage.py initialize_buckets - python manage.py migrate python manage.py collectstatic --no-input + python manage.py compilemessages (python manage.py createsuperuser --no-input || true) python -m debugpy --wait-for-client --listen 0.0.0.0:5678 -m uvicorn backend.asgi:application --host 0.0.0.0 --workers 1 --timeout-keep-alive 300 --ws wsproto --loop uvloop --http httptools --lifespan off --log-level debug --reload volumes: @@ -23,6 +23,8 @@ services: env_file: - .env depends_on: + migrator: + condition: service_completed_successfully cache-mdb: condition: service_started s3: @@ -30,6 +32,19 @@ services: db: condition: service_started + migrator: + restart: on-failure:1 + container_name: migrator + build: + context: . + dockerfile: Dockerfile.dev + command: + - /bin/sh + - -c + - python manage.py migrate + env_file: + - .env + cache-mdb: container_name: cache-mdb image: redis:alpine @@ -3,10 +3,17 @@ services: app: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + image: $CI_REGISTRY_IMAGE:latest build: context: . dockerfile: Dockerfile + tags: + - $CI_REGISTRY_IMAGE:latest + - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + cache_from: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true + cache_to: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true volumes: - static:/code/static command: @@ -23,7 +30,7 @@ services: replicas: 1 update_config: parallelism: 1 - delay: 10s + delay: 1s order: start-first restart_policy: condition: on-failure @@ -48,10 +55,17 @@ services: - $ENV migrator: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + image: $CI_REGISTRY_IMAGE:latest build: context: . dockerfile: Dockerfile + tags: + - $CI_REGISTRY_IMAGE:latest + - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + cache_from: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true + cache_to: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true deploy: replicas: 1 restart_policy: @@ -67,10 +81,17 @@ services: - $ENV celery: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + image: $CI_REGISTRY_IMAGE:latest build: context: . dockerfile: Dockerfile + tags: + - $CI_REGISTRY_IMAGE:latest + - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + cache_from: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true + cache_to: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true command: celery -A backend worker -l INFO --concurrency 8 networks: - default @@ -94,10 +115,17 @@ services: - C_FORCE_ROOT=true celery_beat: - image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + image: $CI_REGISTRY_IMAGE:latest build: context: . dockerfile: Dockerfile + tags: + - $CI_REGISTRY_IMAGE:latest + - $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + cache_from: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,ignore-error=true + cache_to: + - type=registry,ref=$CI_REGISTRY_IMAGE/cache,mode=max,ignore-error=true command: celery -A backend beat -l INFO networks: - default