@@ -0,0 +1,31 @@
+# Generated by Django 5.0.9 on 2025-01-08 14:19
+
+import django.db.models.deletion
+import uuid
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('authentication', '0014_alter_businessaccount_options_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='UserSetting',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')),
+ ('device', models.CharField(choices=[('mobile', 'Телефон'), ('desktop', 'ПК')], verbose_name='Устройство')),
+ ('type', models.CharField(choices=[('sidebar', 'Сайдбар')], verbose_name='Тип')),
+ ('value', models.JSONField(verbose_name='Значение')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_settings', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
+ ],
+ options={
+ 'verbose_name': 'Настройка пользователя',
+ 'verbose_name_plural': 'Настройки пользователя',
+ 'unique_together': {('user', 'type', 'device')},
+ },
+ ),
+ ]
@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from authentication.models.business_group import BusinessGroup
from authentication.models.choices import AccountPrivileges, InvitationStatus
-from ml_model.models import BaseModel
+from core.models import BaseModel
class BusinessAccount(BaseModel):
@@ -10,7 +10,7 @@ from authentication.models.business_account import BusinessAccount
from authentication.models.business_group import BusinessGroup
from authentication.models.choices import BusinessSector, UsageIntensity
from authentication.models.whitelist import CompanyIPWhitelist
-from ml_model.models import BaseModel
+from core.models import BaseModel
class BusinessUserHost(BaseModel):
@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
-from ml_model.models import BaseModel
+from core.models import BaseModel
class EmailToken(BaseModel):
@@ -1,5 +1,6 @@
import random
from typing import TYPE_CHECKING, Optional
+from uuid import uuid4
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from django.core.exceptions import ObjectDoesNotExist
@@ -236,3 +237,32 @@ def on_user_creation_signal(sender, instance, created, **kwargs):
instance.last_name, None
)
instance.save()
+
+
+class UserSetting(models.Model):
+ class DeviceChoices(models.TextChoices):
+ MOBILE = 'mobile', 'Телефон'
+ DESKTOP = 'desktop', 'ПК'
+
+ class TypeChoices(models.TextChoices):
+ SIDEBAR = 'sidebar', 'Сайдбар'
+
+ id = models.UUIDField(
+ primary_key=True, editable=False, default=uuid4, verbose_name='ID'
+ )
+
+ user = models.ForeignKey(
+ CustomUserModel,
+ on_delete=models.CASCADE,
+ related_name='user_settings',
+ verbose_name='Пользователь',
+ )
+
+ device = models.CharField(choices=DeviceChoices.choices, verbose_name='Устройство')
+ type = models.CharField(choices=TypeChoices.choices, verbose_name='Тип')
+ value = models.JSONField(verbose_name='Значение')
+
+ class Meta:
+ verbose_name = 'Настройка пользователя'
+ verbose_name_plural = 'Настройки пользователя'
+ unique_together = ['user', 'type', 'device']
@@ -0,0 +1,30 @@
+from typing import List
+from uuid import UUID
+
+from ninja import Query, Router
+
+from authentication.schemas import (
+ CreateUserSettingSchema,
+ UpdateUserSettingSchema,
+ UserSettingFilterSchema,
+ UserSettingSchema,
+)
+from authentication.security import SyncAuthBearer
+from authentication.services.user_services import UserService
+
+router = Router(auth=SyncAuthBearer(), tags=['users'])
+
+
+@router.get('settings/', tags=['users/settings'], response=List[UserSettingSchema])
+def get_user_settings(request, filters: UserSettingFilterSchema = Query(...)):
+ return UserService.list_settings(filters=filters.get_filter_expression())
+
+
+@router.post('settings/', tags=['users/settings'], response=UserSettingSchema)
+def add_setting(request, data: CreateUserSettingSchema):
+ return UserService.add_setting(user_id=request.auth.uid, **data.model_dump())
+
+
+@router.put('settings/{id}', tags=['users/settings'], response={204: None})
+def update_setting(request, id: UUID, data: UpdateUserSettingSchema):
+ UserService.update_setting(setting_id=id, **data.model_dump())
@@ -1,6 +1,8 @@
-from typing import Tuple
+from typing import Any, Tuple
+from uuid import UUID
from django.contrib.auth import authenticate, login, logout
+from django.db.models import Q, QuerySet
from django.db.transaction import atomic
from django.utils.translation import gettext_lazy as _
from rest_framework.request import Request
@@ -13,6 +15,7 @@ from authentication.models import (
TelegramUser,
VKUser,
)
+from authentication.models.user import UserSetting
from authentication.selectors.email_token_selector import EmailTokenSelector
from authentication.selectors.user_selector import UserSelector
from authentication.serializers import (
@@ -280,6 +283,22 @@ class UserService:
emails__contains=[request.query_params['email']]
).exists()
+ @classmethod
+ def list_settings(cls, filters: Q = Q()) -> QuerySet[UserSetting]:
+ return UserSetting.objects.filter(filters)
+
+ @classmethod
+ def add_setting(
+ cls, user_id: UUID, device: str, type: str, value: Any
+ ) -> UserSetting:
+ return UserSetting.objects.create(
+ user_id=user_id, device=device, type=type, value=value
+ )
+
+ @classmethod
+ def update_setting(cls, setting_id: UUID, value: Any) -> None:
+ UserSetting.objects.filter(id=setting_id).update(value=value)
+
# For Social Auth pipeline
def update_profile_picture_social(*args, **kwargs):
@@ -0,0 +1,28 @@
+from typing import List, Optional
+
+from ninja import Field, FilterSchema, ModelSchema
+
+from authentication.models.user import UserSetting
+
+
+class UserSettingFilterSchema(FilterSchema):
+ device: Optional[str] = None
+ types: List[str] = Field(None, q='type__in')
+
+
+class UserSettingSchema(ModelSchema):
+ class Meta:
+ model = UserSetting
+ exclude = ('user',)
+
+
+class CreateUserSettingSchema(ModelSchema):
+ class Meta:
+ model = UserSetting
+ fields = ('device', 'type', 'value')
+
+
+class UpdateUserSettingSchema(ModelSchema):
+ class Meta:
+ model = UserSetting
+ fields = ('value',)
@@ -8,7 +8,7 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_asgi_application()
-from tools.copywrite.router import ws_router as copywrite_ws_router # noqa: E402
+from tools.copywrite.routes.v1 import ws_router as copywrite_ws_router # noqa: E402
application = ProtocolTypeRouter(
{
@@ -38,6 +38,7 @@ CORE_APP = 'core.apps.CoreConfig'
SYSTEM_APPS = [
'django.contrib.auth',
+ 'polymorphic',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
@@ -1,14 +1,40 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
+from django.core.exceptions import ObjectDoesNotExist
from django.urls import include, path
+from django.utils.translation import gettext_lazy as _
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from ninja import NinjaAPI
+from authentication.exceptions import InvalidPassword, InvalidToken, InvalidUsername
from backend.public import urlpatterns as public_urlpatterns
-api = NinjaAPI(title='AIR API', version='2.0.0')
-api.add_router('copywrite/', 'tools.copywrite.router.router')
+api = NinjaAPI(title='AIR API', version='1.0.0')
+api.add_router('copywrite/', 'tools.copywrite.routes.v1.router')
+api.add_router('users/', 'authentication.routes.v1.router')
+
+
+@api.exception_handler(ObjectDoesNotExist)
+def object_does_not_exists_error_handler(request, exc: ObjectDoesNotExist):
+ return api.create_response(
+ request, {'message': _('Requested object does not exists')}, status=404
+ )
+
+
+@api.exception_handler(InvalidToken)
+def invalid_token_error_handler(request, exc: InvalidToken):
+ return api.create_response(request, {'message': _('Token is invalid')}, status=401)
+
+
+@api.exception_handler(InvalidPassword)
+def invalid_password_error_handler(request, exc: InvalidPassword):
+ return api.create_response(request, {'message': _('Wrong password')}, status=401)
+
+
+@api.exception_handler(InvalidUsername)
+def invalid_username_error_handler(request, exc: InvalidUsername):
+ return api.create_response(request, {'message': _('Wrong username')}, status=401)
urlpatterns = [
@@ -0,0 +1,33 @@
+ALPHABET = {
+ 'а': 'a',
+ 'б': 'b',
+ 'в': 'v',
+ 'г': 'g',
+ 'д': 'd',
+ 'е': 'e',
+ 'ё': 'yo',
+ 'ж': 'zh',
+ 'з': 'z',
+ 'и': 'i',
+ 'й': 'j',
+ 'к': 'k',
+ 'л': 'l',
+ 'м': 'm',
+ 'н': 'n',
+ 'о': 'o',
+ 'п': 'p',
+ 'р': 'r',
+ 'с': 's',
+ 'т': 't',
+ 'у': 'u',
+ 'ф': 'f',
+ 'х': 'kh',
+ 'ц': 'ts',
+ 'ч': 'ch',
+ 'ш': 'sh',
+ 'щ': 'shch',
+ 'ы': 'i',
+ 'э': 'e',
+ 'ю': 'yu',
+ 'я': 'ya',
+}
@@ -0,0 +1,7 @@
+from django.template.defaultfilters import slugify as django_slugify
+
+from core.constants import ALPHABET
+
+
+def slugify(s: str):
+ return django_slugify(''.join(ALPHABET.get(w, w) for w in s.lower()))
@@ -0,0 +1,55 @@
+# Generated by Django 5.0.9 on 2025-01-09 09:29
+
+import django.db.models.deletion
+import uuid
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('ml_model', '0039_alter_neuronmodel_slug'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='modelcategory',
+ name='title',
+ field=models.CharField(max_length=100, verbose_name='Title'),
+ ),
+ migrations.AlterField(
+ model_name='modelversion',
+ name='slug',
+ field=models.CharField(max_length=32, unique=True, verbose_name='Slug'),
+ ),
+ migrations.CreateModel(
+ name='ModelConfiguration',
+ fields=[
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')),
+ ('oid', models.UUIDField()),
+ ('ct', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
+ ('model', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='model_configurations', to='ml_model.neuronmodel', to_field='slug', verbose_name='Модель')),
+ ('version', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='version_configurations', to='ml_model.modelversion', verbose_name='Версия модели')),
+ ],
+ options={
+ 'verbose_name': 'Конфигурация модели',
+ 'verbose_name_plural': 'Конфигурации моделей',
+ 'unique_together': {('oid', 'ct')},
+ },
+ ),
+ migrations.CreateModel(
+ name='ConfigurationParameter',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('value', models.JSONField(verbose_name='Значение')),
+ ('parameter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='model_configuration_parameters', to='ml_model.modelparameter', verbose_name='Параметр модели')),
+ ('configuration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='configuration_parameters', to='ml_model.modelconfiguration', verbose_name='Конфигурация')),
+ ],
+ options={
+ 'verbose_name': 'Параметр конфигурации',
+ 'verbose_name_plural': 'Параметры конфигураций',
+ 'unique_together': {('configuration', 'parameter')},
+ },
+ ),
+ ]
@@ -0,0 +1,19 @@
+# Generated by Django 5.0.9 on 2025-01-09 09:29
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ml_model', '0040_alter_modelcategory_title_alter_modelversion_slug_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='modelconfiguration',
+ name='version',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='version_configurations', to='ml_model.modelversion', to_field='slug', verbose_name='Версия модели'),
+ ),
+ ]
@@ -1,11 +1,16 @@
import itertools
+import base64
import time
+import logging
+
from datetime import timedelta
from decimal import Decimal
+from math import ceil
from io import BufferedReader, BytesIO
from typing import Any, Dict, Generator, List, Optional
-from django.core.files.uploadedfile import InMemoryUploadedFile
+import filetype
+import tiktoken
from langchain import hub
from langchain.agents import AgentExecutor, create_structured_chat_agent
from langchain.chains import ConversationChain
@@ -17,9 +22,15 @@ from langchain_openai.chat_models import ChatOpenAI
from PIL import Image
from backend import settings
-from messages.models import Message
from ml_model.constants import TEMPORARY_TEST_TEXT
-from ml_model.models import ModelCategory, ModelInput, ModelParameter, ModelVersion
+from messages.models import Message, BaseStore
+from ml_model.models import (
+ ModelCategory,
+ ModelConfiguration,
+ ModelInput,
+ ModelParameter,
+ ModelVersion,
+)
from ml_model.services.base import SimpleService
from payments.exceptions.insufficient_balance import InsufficientBalance
from payments.selectors.payment_plan_selector import PaymentPlanSelector
@@ -43,9 +54,6 @@ class Chatgpt(SimpleService):
ModelVersion(name='GPT-4o1 Mini', slug='o1-mini'),
ModelVersion(name='GPT-4omni Mini', slug='gpt-4o-mini'),
ModelVersion(name='GPT-4omni', slug='gpt-4o'),
- ModelVersion(name='GPT-4 Turbo', slug='gpt-4-turbo'),
- ModelVersion(name='GPT-4', slug='gpt-4'),
- ModelVersion(name='GPT-3.5', slug='gpt-3.5-turbo'),
]
inputs = [
@@ -82,15 +90,16 @@ class Chatgpt(SimpleService):
category = ModelCategory(title='Чат-боты', slug='chat-bots')
TOKENS_COST = {
- 'gpt-3.5-turbo': Decimal('0.0085'),
- 'gpt-4': Decimal('0.08925'),
- 'gpt-4-turbo': Decimal('0.25'),
- 'gpt-4o': Decimal('0.075'),
- 'o1-preview': Decimal('0.09'),
- 'o1-mini': Decimal('0.02'),
- 'gpt-4o-mini': Decimal('0.03'),
+ 'o1-preview': {'input': Decimal('0.008250'), 'output': Decimal('0.033000')},
+ 'o1-mini': {'input': Decimal('0.001650'), 'output': Decimal('0.006600')},
+ 'gpt-4o-mini': {'input': Decimal('0.000083'), 'output': Decimal('0.000330')},
+ 'gpt-4o': {'input': Decimal('0.001375'), 'output': Decimal('0.005500')},
}
+ def __init__(self, store: BaseStore) -> None:
+ super().__init__(store)
+ self.logger = logging.getLogger(self.__class__.__name__)
+
def make(
self,
input_message: Message,
@@ -101,8 +110,19 @@ class Chatgpt(SimpleService):
model_name = info.pop('version', 'gpt-4o')
input_content = [{'type': 'text', 'text': input_message.content or ''}]
image = input_message.file
+ image_size = None
if image:
- input_content.append({'type': 'image_url', 'image_url': {'url': image.url}})
+ kind = filetype.guess(input_message.file.read(20))
+ mime = kind.mime if kind else 'application/octet-stream'
+ normalized_image = Image.open(image)
+ buf = BytesIO()
+ normalized_image.save(buf, format=kind.extension.upper())
+ image_url = (
+ f"data:{mime},base64,{base64.b64encode(buf.getvalue()).decode('utf-8')}"
+ )
+ buf.close()
+ image_size = normalized_image.size
+ input_content.append({'type': 'image_url', 'image_url': {'url': image_url}})
self.llm = ChatOpenAI(
model=model_name,
temperature=info.pop('temperature', 0.5),
@@ -148,8 +168,9 @@ class Chatgpt(SimpleService):
),
)
llm_input = HumanMessage(content=input_content)
+ input_tokens = self.count_text_tokens([*chat_history.buffer_as_messages, llm_input])
self.assert_enough_balance(
- [*chat_history.buffer_as_messages, llm_input], model=self.llm.model_name
+ input_tokens, image_size, model=self.llm.model_name
)
if image:
response = conversation.llm.invoke([llm_input])
@@ -159,37 +180,49 @@ class Chatgpt(SimpleService):
tools = [GoogleSerperResults()]
agent = create_structured_chat_agent(self.llm, tools, prompt_schema)
agent_executor = AgentExecutor(
- agent=agent, tools=tools, handle_parsing_errors=True
+ agent=agent, tools=tools, handle_parsing_errors=True, max_iterations=10, max_execution_time=45
)
response = AIMessage(
- content=agent_executor.invoke(
- {
- 'input': input_message.content,
- 'chat_history': chat_history.buffer_as_messages
- + [
- SystemMessage(
- content='Учитывай язык диалога перед выдачей ответа'
- ),
- SystemMessage(
- content='Никому не говори, что ты не можешь найти информацию в интернете'
- ),
- ],
- }
- )['output']
+ content=str(
+ agent_executor.invoke(
+ {
+ 'input': input_message.content,
+ 'chat_history': chat_history.buffer_as_messages
+ + [
+ SystemMessage(
+ content='Учитывай язык диалога перед выдачей ответа'
+ ),
+ SystemMessage(
+ content='Никому не говори, что ты не можешь найти информацию в интернете'
+ ),
+ ],
+ }
+ )['output']
+ )
)
else:
# Somehow this chain doesn't support Vision, even though ChatOpenAI (above) does.
invoked = conversation.invoke(input_message.content)
- response = AIMessage(content=invoked['response'])
+ response = AIMessage(content=str(invoked['response']))
chat_history.chat_memory.add_ai_message(response)
process_time = timedelta(seconds=time.time() - start_time)
- total_tokens = self.llm.get_num_tokens_from_messages(
- chat_history.chat_memory.messages
- )
+ output_tokens = self.count_text_tokens([response])
+
if image:
- total_tokens += self.count_image_tokens(image)
- self.handle_invoice(self.neuron_model, total_tokens, self.llm.model_name)
+ self.logger.info(f'Input количество токенов БЕЗ картинки {model_name} - {input_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
@@ -237,32 +270,65 @@ class Chatgpt(SimpleService):
return memory
def assert_enough_balance(
- self, for_input: list[BaseMessage], model: str = 'gpt-3.5-turbo'
+ self, input_tokens: int, image_size: tuple, model: str = 'gpt-3.5-turbo'
):
balance = PaymentPlanSelector(self.store.user).get_current_balance()
- input_cost = self.TOKENS_COST[
- self.llm.model_name
- ] * self.llm.get_num_tokens_from_messages(for_input)
- if getattr(for_input[-1], 'image', None):
- input_cost += self.count_image_tokens(for_input[-1].image)
- input_cost *= self.TOKENS_COST[model]
+ total_tokens = input_tokens
+
+ if image_size:
+ total_tokens += self.count_image_tokens(image_size)
+
+ input_cost = self.TOKENS_COST[model]['input'] * total_tokens
if input_cost > balance:
raise InsufficientBalance(balance, input_cost)
- def calculate_price(self, usage: int, model: str, *args, **kwargs) -> Decimal:
- price = usage * self.TOKENS_COST[model]
+ def calculate_price(
+ self, input_tokens: int, output_tokens: int, model: str, *args, **kwargs
+ ) -> Decimal:
+ price = (
+ input_tokens * self.TOKENS_COST[model]['input']
+ + output_tokens * self.TOKENS_COST[model]['output']
+ )
return price.quantize(Decimal('0.1'), rounding='ROUND_UP')
def count_image_tokens(
- self, image: InMemoryUploadedFile, high_resolution: bool = True
+ self, image_size: tuple, model_version: str = 'gpt-4o'
) -> int:
- TILE_SIZE = 512 * 512
- RESOLUTION_MULTIPLIER = 170 if high_resolution else 85
- minio_image = Image.open(BytesIO(image.read()))
- width, height = minio_image.size
- area = width * height
- num_of_tiles = (area // TILE_SIZE) + 1
- return num_of_tiles * RESOLUTION_MULTIPLIER + 85
+ extra_tokens = {
+ 'gpt-4o': {
+ 'tile_tokens': 170,
+ 'base_tokens': 85,
+ },
+ 'gpt-4o-mini': {
+ 'tile_tokens': 5667,
+ 'base_tokens': 2833,
+ }
+ }
+ width, height = image_size
+
+ if max(width, height) > 2048:
+ a_ratio = width / height
+ width, height = (2048, int(2048 / a_ratio)) if a_ratio > 1 else (int(2048 * a_ratio), 2048)
+ if width >= height and height > 768:
+ width, height = int((768 / height) * width), 768
+ elif height > width and width > 768:
+ width, height = 768, int((768 / width) * height)
+ tiles_size = ceil(width / 512) * ceil(height / 512)
+
+ return extra_tokens[model_version]['base_tokens'] + extra_tokens[model_version]['tile_tokens'] * tiles_size
+
+ def count_text_tokens(self, messages: list[BaseMessage]) -> int:
+ encoding = tiktoken.get_encoding("cl100k_base")
+ total_tokens = 0
+
+ for message in messages:
+ total_tokens += len(
+ encoding.encode(
+ message.content if isinstance(message.content, str)
+ else message.content[0]['text']
+ )
+ )
+ return total_tokens
def save_results(
self, results: list[BaseMessage], elapsed_time: timedelta, save: bool = True
@@ -324,16 +390,27 @@ class Chatgpt(SimpleService):
image: Optional[BufferedReader] = None,
context_messages: List[str] = [],
*,
+ configuration: Optional[ModelConfiguration] = None,
stream: bool = False,
- parameters: List[Dict[str, Any]] = [],
) -> str | Generator[str, None, None]:
# with httpx.Client() as client:
# data = {}
- # if not stream:
- # client.post('https://openai.com/api/chat/completions', json=data)
- # resp = client.post(
- # 'https://openai.com/api/chat/completions', json=data | {'stream': True}
- # )
+ # with client.stream(
+ # 'POST',
+ # 'https://api.openai.com/v1/chat/completions',
+ # headers={'Authorization': f'Bearer {settings.OPENAI_API_KEY}'},
+ # json={
+ # 'model': 'gpt-4o-mini',
+ # 'messages': [
+ # {
+ # 'role': 'user',
+ # 'content': 'Hello! Generate a tale for 1500 symbols',
+ # }
+ # ],
+ # 'stream': True,
+ # },
+ # ) as resp:
+ # ...
+
for chunk in itertools.batched(TEMPORARY_TEST_TEXT, 10):
- time.sleep(0.3)
yield ''.join(chunk)
@@ -8,7 +8,6 @@ from django.core.files import File
from backend import settings
from messages.models import BaseStore, Message
-from ml_model.exceptions.external_api import ExternalAPIException
from ml_model.models import (
ModelCategory,
ModelInput,
@@ -123,7 +122,7 @@ class Recraft(SimpleService):
}
data = {'input': payload}
response = requests.post(
- url=f'{self.generate_url}{payload['version']}/predictions',
+ url=f'{self.generate_url}{payload.get('version', 'recraft-v3')}/predictions',
headers=headers,
json=data,
)
@@ -186,7 +185,7 @@ class Recraft(SimpleService):
'линогравюра': 'linocut'
}
start_time = time.time()
- extension = '.svg' if input_message.info['version'] == self.versions[1].slug else '.png'
+ extension = '.svg' if input_message.info.get('version', 'recraft-v3') == self.versions[1].slug else '.png'
size = self._get_size(input_message.info.pop('width', 1024), input_message.info.pop('height', 1024))
callback_data = dict(
{
@@ -195,7 +194,7 @@ class Recraft(SimpleService):
f'Input description: {self.translate_prompt(input_message.content)}'
),
'size': size,
- 'style': styles.get(input_message.info.pop('style'), 'любой'),
+ 'style': styles.get(input_message.info.get('style', 'любой')),
**input_message.info
}
)
@@ -1,6 +1,6 @@
from django.contrib import admin
from django.http.response import HttpResponse as HttpResponse
-from import_export.admin import ExportActionModelAdmin, ImportExportModelAdmin
+from import_export.admin import ImportExportMixin
from ordered_model.admin import (
OrderedInlineModelAdminMixin,
OrderedModelAdmin,
@@ -8,7 +8,9 @@ from ordered_model.admin import (
)
from ml_model.models import (
+ ConfigurationParameter,
ModelCategory,
+ ModelConfiguration,
ModelInput,
ModelParameter,
ModelPaymentRule,
@@ -65,10 +67,7 @@ class ModelCategoryAdmin(admin.ModelAdmin):
@admin.register(NeuronModel)
class NeuronModelModelAdmin(
- OrderedInlineModelAdminMixin,
- ImportExportModelAdmin,
- ExportActionModelAdmin,
- OrderedModelAdmin,
+ OrderedInlineModelAdminMixin, ImportExportMixin, OrderedModelAdmin
):
list_display = ['title', 'is_active', 'category', 'move_up_down_links']
resource_classes = (NeuronModelResource,)
@@ -89,3 +88,18 @@ class NeuronModelModelAdmin(
@admin.display(description='Категория', boolean=True)
def category(self, obj: NeuronModel):
return obj.category.title
+
+
+class ConfigurationParameterInline(admin.TabularInline):
+ model = ConfigurationParameter
+ extra = 1
+ can_delete = False
+
+ def has_change_permission(self, request, obj=...):
+ return False
+
+
+@admin.register(ModelConfiguration)
+class ModelConfigurationAdmin(admin.ModelAdmin):
+ fields = ('model', 'version')
+ inlines = (ConfigurationParameterInline,)
@@ -5,6 +5,55 @@ dolor sit amet, consectetur adipiscing elit. Proin venenatis ornare porta. Vivam
Aenean aliquet finibus lobortis.
Nam convallis mi a luctus laoreet. Etiam suscipit egestas posuere. Aenean eget facilisis augue, ut pulvinar lacus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc pharetra nisl non nulla tristique gravida. Phasellus nunc eros, cursus in nibh a, suscipit vehicula justo. Sed in mi in nulla efficitur faucibus. Maecenas at vestibulum nunc, ultricies elementum sapien. Proin pulvinar ultrices placerat. Duis convallis lobortis pellentesque. Donec ornare ante mauris, a consectetur magna lobortis consequat. Quisque gravida eget neque eleifend bibendum.
+Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec rutrum viverra suscipit. Etiam nec laoreet quam. Aenean in ligula sit amet nibh consectetur porta. Cras molestie nisl a odio lacinia, ac ornare odio hendrerit. Mauris et pulvinar dolor. Morbi pretium luctus sem, ac congue eros ornare eget. Pellentesque laoreet elementum felis id mollis. Maecenas velit nulla, gravida sodales lectus in, fermentum auctor justo. Sed euismod maximus ex, vel pharetra massa ultricies vel. Curabitur interdum cursus quam ut placerat. Phasellus eleifend purus sed nisi pellentesque condimentum. Fusce finibus, felis quis mattis tempus, tortor elit tincidunt massa, non interdum tellus neque id mauris. Morbi dapibus volutpat lectus sed pellentesque. Curabitur sagittis lacinia quam eu viverra.
+In sit amet nunc sed urna aliquet vehicula id vel justo. Duis vel massa eleifend, tristique ante in, ullamcorper tellus. Donec egestas lacus eu libero sodales luctus. Duis id maximus arcu, non dictum felis. Cras nisl odio, viverra in arcu at, sagittis gravida augue. Aliquam quis metus vel urna finibus sodales. Praesent a porttitor magna. Integer eu est ac ligula luctus consequat.
+
+Lorem ipsum
+dolor sit amet, consectetur adipiscing elit. Proin venenatis ornare porta. Vivamus laoreet, eros vitae consectetur semper, nisi purus dignissim libero, et aliquam nisi sem eu nibh. Praesent malesuada augue eget purus interdum congue. Vestibulum ut maximus dolor. Donec vitae enim et mi ultrices faucibus. Vestibulum quis sollicitudin arcu, eget molestie sapien. Pellentesque id eros erat. Vestibulum vitae dapibus diam, non ornare eros. Maecenas gravida vulputate mi, id auctor sapien faucibus in.
+
+Aenean aliquet finibus lobortis.
+Nam convallis mi a luctus laoreet. Etiam suscipit egestas posuere. Aenean eget facilisis augue, ut pulvinar lacus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc pharetra nisl non nulla tristique gravida. Phasellus nunc eros, cursus in nibh a, suscipit vehicula justo. Sed in mi in nulla efficitur faucibus. Maecenas at vestibulum nunc, ultricies elementum sapien. Proin pulvinar ultrices placerat. Duis convallis lobortis pellentesque. Donec ornare ante mauris, a consectetur magna lobortis consequat. Quisque gravida eget neque eleifend bibendum.
+
+Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec rutrum viverra suscipit. Etiam nec laoreet quam. Aenean in ligula sit amet nibh consectetur porta. Cras molestie nisl a odio lacinia, ac ornare odio hendrerit. Mauris et pulvinar dolor. Morbi pretium luctus sem, ac congue eros ornare eget. Pellentesque laoreet elementum felis id mollis. Maecenas velit nulla, gravida sodales lectus in, fermentum auctor justo. Sed euismod maximus ex, vel pharetra massa ultricies vel. Curabitur interdum cursus quam ut placerat. Phasellus eleifend purus sed nisi pellentesque condimentum. Fusce finibus, felis quis mattis tempus, tortor elit tincidunt massa, non interdum tellus neque id mauris. Morbi dapibus volutpat lectus sed pellentesque. Curabitur sagittis lacinia quam eu viverra.
+In sit amet nunc sed urna aliquet vehicula id vel justo. Duis vel massa eleifend, tristique ante in, ullamcorper tellus. Donec egestas lacus eu libero sodales luctus. Duis id maximus arcu, non dictum felis. Cras nisl odio, viverra in arcu at, sagittis gravida augue. Aliquam quis metus vel urna finibus sodales. Praesent a porttitor magna. Integer eu est ac ligula luctus consequat.
+
+
+Lorem ipsum
+dolor sit amet, consectetur adipiscing elit. Proin venenatis ornare porta. Vivamus laoreet, eros vitae consectetur semper, nisi purus dignissim libero, et aliquam nisi sem eu nibh. Praesent malesuada augue eget purus interdum congue. Vestibulum ut maximus dolor. Donec vitae enim et mi ultrices faucibus. Vestibulum quis sollicitudin arcu, eget molestie sapien. Pellentesque id eros erat. Vestibulum vitae dapibus diam, non ornare eros. Maecenas gravida vulputate mi, id auctor sapien faucibus in.
+
+Aenean aliquet finibus lobortis.
+Nam convallis mi a luctus laoreet. Etiam suscipit egestas posuere. Aenean eget facilisis augue, ut pulvinar lacus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc pharetra nisl non nulla tristique gravida. Phasellus nunc eros, cursus in nibh a, suscipit vehicula justo. Sed in mi in nulla efficitur faucibus. Maecenas at vestibulum nunc, ultricies elementum sapien. Proin pulvinar ultrices placerat. Duis convallis lobortis pellentesque. Donec ornare ante mauris, a consectetur magna lobortis consequat. Quisque gravida eget neque eleifend bibendum.
+
+Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec rutrum viverra suscipit. Etiam nec laoreet quam. Aenean in ligula sit amet nibh consectetur porta. Cras molestie nisl a odio lacinia, ac ornare odio hendrerit. Mauris et pulvinar dolor. Morbi pretium luctus sem, ac congue eros ornare eget. Pellentesque laoreet elementum felis id mollis. Maecenas velit nulla, gravida sodales lectus in, fermentum auctor justo. Sed euismod maximus ex, vel pharetra massa ultricies vel. Curabitur interdum cursus quam ut placerat. Phasellus eleifend purus sed nisi pellentesque condimentum. Fusce finibus, felis quis mattis tempus, tortor elit tincidunt massa, non interdum tellus neque id mauris. Morbi dapibus volutpat lectus sed pellentesque. Curabitur sagittis lacinia quam eu viverra.
+In sit amet nunc sed urna aliquet vehicula id vel justo. Duis vel massa eleifend, tristique ante in, ullamcorper tellus. Donec egestas lacus eu libero sodales luctus. Duis id maximus arcu, non dictum felis. Cras nisl odio, viverra in arcu at, sagittis gravida augue. Aliquam quis metus vel urna finibus sodales. Praesent a porttitor magna. Integer eu est ac ligula luctus consequat.
+
+
+Lorem ipsum
+dolor sit amet, consectetur adipiscing elit. Proin venenatis ornare porta. Vivamus laoreet, eros vitae consectetur semper, nisi purus dignissim libero, et aliquam nisi sem eu nibh. Praesent malesuada augue eget purus interdum congue. Vestibulum ut maximus dolor. Donec vitae enim et mi ultrices faucibus. Vestibulum quis sollicitudin arcu, eget molestie sapien. Pellentesque id eros erat. Vestibulum vitae dapibus diam, non ornare eros. Maecenas gravida vulputate mi, id auctor sapien faucibus in.
+
+Aenean aliquet finibus lobortis.
+Nam convallis mi a luctus laoreet. Etiam suscipit egestas posuere. Aenean eget facilisis augue, ut pulvinar lacus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc pharetra nisl non nulla tristique gravida. Phasellus nunc eros, cursus in nibh a, suscipit vehicula justo. Sed in mi in nulla efficitur faucibus. Maecenas at vestibulum nunc, ultricies elementum sapien. Proin pulvinar ultrices placerat. Duis convallis lobortis pellentesque. Donec ornare ante mauris, a consectetur magna lobortis consequat. Quisque gravida eget neque eleifend bibendum.
+
+Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec rutrum viverra suscipit. Etiam nec laoreet quam. Aenean in ligula sit amet nibh consectetur porta. Cras molestie nisl a odio lacinia, ac ornare odio hendrerit. Mauris et pulvinar dolor. Morbi pretium luctus sem, ac congue eros ornare eget. Pellentesque laoreet elementum felis id mollis. Maecenas velit nulla, gravida sodales lectus in, fermentum auctor justo. Sed euismod maximus ex, vel pharetra massa ultricies vel. Curabitur interdum cursus quam ut placerat. Phasellus eleifend purus sed nisi pellentesque condimentum. Fusce finibus, felis quis mattis tempus, tortor elit tincidunt massa, non interdum tellus neque id mauris. Morbi dapibus volutpat lectus sed pellentesque. Curabitur sagittis lacinia quam eu viverra.
+In sit amet nunc sed urna aliquet vehicula id vel justo. Duis vel massa eleifend, tristique ante in, ullamcorper tellus. Donec egestas lacus eu libero sodales luctus. Duis id maximus arcu, non dictum felis. Cras nisl odio, viverra in arcu at, sagittis gravida augue. Aliquam quis metus vel urna finibus sodales. Praesent a porttitor magna. Integer eu est ac ligula luctus consequat.
+
+
+Lorem ipsum
+dolor sit amet, consectetur adipiscing elit. Proin venenatis ornare porta. Vivamus laoreet, eros vitae consectetur semper, nisi purus dignissim libero, et aliquam nisi sem eu nibh. Praesent malesuada augue eget purus interdum congue. Vestibulum ut maximus dolor. Donec vitae enim et mi ultrices faucibus. Vestibulum quis sollicitudin arcu, eget molestie sapien. Pellentesque id eros erat. Vestibulum vitae dapibus diam, non ornare eros. Maecenas gravida vulputate mi, id auctor sapien faucibus in.
+
+Aenean aliquet finibus lobortis.
+Nam convallis mi a luctus laoreet. Etiam suscipit egestas posuere. Aenean eget facilisis augue, ut pulvinar lacus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc pharetra nisl non nulla tristique gravida. Phasellus nunc eros, cursus in nibh a, suscipit vehicula justo. Sed in mi in nulla efficitur faucibus. Maecenas at vestibulum nunc, ultricies elementum sapien. Proin pulvinar ultrices placerat. Duis convallis lobortis pellentesque. Donec ornare ante mauris, a consectetur magna lobortis consequat. Quisque gravida eget neque eleifend bibendum.
+
+Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec rutrum viverra suscipit. Etiam nec laoreet quam. Aenean in ligula sit amet nibh consectetur porta. Cras molestie nisl a odio lacinia, ac ornare odio hendrerit. Mauris et pulvinar dolor. Morbi pretium luctus sem, ac congue eros ornare eget. Pellentesque laoreet elementum felis id mollis. Maecenas velit nulla, gravida sodales lectus in, fermentum auctor justo. Sed euismod maximus ex, vel pharetra massa ultricies vel. Curabitur interdum cursus quam ut placerat. Phasellus eleifend purus sed nisi pellentesque condimentum. Fusce finibus, felis quis mattis tempus, tortor elit tincidunt massa, non interdum tellus neque id mauris. Morbi dapibus volutpat lectus sed pellentesque. Curabitur sagittis lacinia quam eu viverra.
+In sit amet nunc sed urna aliquet vehicula id vel justo. Duis vel massa eleifend, tristique ante in, ullamcorper tellus. Donec egestas lacus eu libero sodales luctus. Duis id maximus arcu, non dictum felis. Cras nisl odio, viverra in arcu at, sagittis gravida augue. Aliquam quis metus vel urna finibus sodales. Praesent a porttitor magna. Integer eu est ac ligula luctus consequat.
+
+
+Lorem ipsum
+dolor sit amet, consectetur adipiscing elit. Proin venenatis ornare porta. Vivamus laoreet, eros vitae consectetur semper, nisi purus dignissim libero, et aliquam nisi sem eu nibh. Praesent malesuada augue eget purus interdum congue. Vestibulum ut maximus dolor. Donec vitae enim et mi ultrices faucibus. Vestibulum quis sollicitudin arcu, eget molestie sapien. Pellentesque id eros erat. Vestibulum vitae dapibus diam, non ornare eros. Maecenas gravida vulputate mi, id auctor sapien faucibus in.
+
+Aenean aliquet finibus lobortis.
+Nam convallis mi a luctus laoreet. Etiam suscipit egestas posuere. Aenean eget facilisis augue, ut pulvinar lacus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc pharetra nisl non nulla tristique gravida. Phasellus nunc eros, cursus in nibh a, suscipit vehicula justo. Sed in mi in nulla efficitur faucibus. Maecenas at vestibulum nunc, ultricies elementum sapien. Proin pulvinar ultrices placerat. Duis convallis lobortis pellentesque. Donec ornare ante mauris, a consectetur magna lobortis consequat. Quisque gravida eget neque eleifend bibendum.
+
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec rutrum viverra suscipit. Etiam nec laoreet quam. Aenean in ligula sit amet nibh consectetur porta. Cras molestie nisl a odio lacinia, ac ornare odio hendrerit. Mauris et pulvinar dolor. Morbi pretium luctus sem, ac congue eros ornare eget. Pellentesque laoreet elementum felis id mollis. Maecenas velit nulla, gravida sodales lectus in, fermentum auctor justo. Sed euismod maximus ex, vel pharetra massa ultricies vel. Curabitur interdum cursus quam ut placerat. Phasellus eleifend purus sed nisi pellentesque condimentum. Fusce finibus, felis quis mattis tempus, tortor elit tincidunt massa, non interdum tellus neque id mauris. Morbi dapibus volutpat lectus sed pellentesque. Curabitur sagittis lacinia quam eu viverra.
In sit amet nunc sed urna aliquet vehicula id vel justo. Duis vel massa eleifend, tristique ante in, ullamcorper tellus. Donec egestas lacus eu libero sodales luctus. Duis id maximus arcu, non dictum felis. Cras nisl odio, viverra in arcu at, sagittis gravida augue. Aliquam quis metus vel urna finibus sodales. Praesent a porttitor magna. Integer eu est ac ligula luctus consequat.
"""
@@ -0,0 +1,12 @@
+from django.contrib.contenttypes.admin import GenericStackedInline
+
+from ml_model.models import ModelConfiguration
+
+
+class ModelConfigurationInline(GenericStackedInline):
+ model = ModelConfiguration
+ ct_fk_field = 'oid'
+ ct_field = 'ct'
+ max_num = 1
+ extra = 0
+ show_change_link = True
@@ -1,3 +1,7 @@
+from uuid import uuid4
+
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import F, QuerySet
from django.utils.translation import gettext_lazy as _
@@ -8,7 +12,7 @@ from core.models import BaseModel
class ModelCategory(models.Model):
- title = models.CharField(max_length=100, blank=True, verbose_name=_('Title'))
+ title = models.CharField(max_length=100, verbose_name=_('Title'))
slug = models.SlugField(max_length=100, unique=True, verbose_name=_('Slug'))
@property
@@ -120,7 +124,7 @@ class ModelVersion(ModelDepends, OrderedModel):
description = models.CharField(
max_length=128, null=True, blank=True, verbose_name=_('Description')
)
- slug = models.CharField(max_length=32, verbose_name=_('Slug'))
+ slug = models.CharField(max_length=32, unique=True, verbose_name=_('Slug'))
default = models.BooleanField(default=False, verbose_name=_('Default'))
order_with_respect_to = 'model'
@@ -277,3 +281,60 @@ class ModelPaymentRule(ModelDepends, ModelVersionsDepends):
class Meta:
verbose_name = _('Payment Rule')
verbose_name_plural = _('Payment Rules')
+
+
+class ModelConfiguration(models.Model):
+ id = models.UUIDField(
+ primary_key=True, default=uuid4, editable=False, verbose_name='ID'
+ )
+ model = models.ForeignKey(
+ NeuronModel,
+ on_delete=models.PROTECT,
+ to_field='slug',
+ related_name='model_configurations',
+ verbose_name='Модель',
+ )
+ version = models.ForeignKey(
+ ModelVersion,
+ on_delete=models.PROTECT,
+ to_field='slug',
+ null=True,
+ blank=True,
+ related_name='version_configurations',
+ verbose_name='Версия модели',
+ )
+
+ ct = models.ForeignKey(ContentType, on_delete=models.CASCADE)
+ oid = models.UUIDField()
+ obj = GenericForeignKey('ct', 'oid')
+
+ @property
+ def parameters(self) -> QuerySet['ConfigurationParameter']:
+ return self.configuration_parameters.all()
+
+ class Meta:
+ verbose_name = 'Конфигурация модели'
+ verbose_name_plural = 'Конфигурации моделей'
+ unique_together = ('oid', 'ct')
+
+
+class ConfigurationParameter(models.Model):
+ configuration = models.ForeignKey(
+ ModelConfiguration,
+ on_delete=models.CASCADE,
+ related_name='configuration_parameters',
+ verbose_name='Конфигурация',
+ )
+
+ parameter = models.ForeignKey(
+ ModelParameter,
+ on_delete=models.CASCADE,
+ related_name='model_configuration_parameters',
+ verbose_name='Параметр модели',
+ )
+ value = models.JSONField(verbose_name='Значение')
+
+ class Meta:
+ verbose_name = 'Параметр конфигурации'
+ verbose_name_plural = 'Параметры конфигураций'
+ unique_together = ['configuration', 'parameter']
@@ -0,0 +1,27 @@
+from typing import List, Optional
+
+from ninja import ModelSchema
+
+from ml_model.models import (
+ ConfigurationParameter,
+ ModelConfiguration,
+)
+
+
+class ConfigurationParameterSchema(ModelSchema):
+ class Meta:
+ model = ConfigurationParameter
+ exclude = ('configuration',)
+
+
+class ModelConfigurationSchema(ModelSchema):
+ parameters: List[ConfigurationParameterSchema] = []
+ model_id: str
+ version_id: Optional[str] = None
+
+ class Meta:
+ model = ModelConfiguration
+ exclude = ('ct', 'oid', 'obj', 'model', 'version')
+
+ class Config:
+ protected_namespaces = ()
@@ -6,7 +6,8 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from django_celery_beat.models import PeriodicTask
-from ml_model.models import BaseModel, NeuronModel
+from core.models import BaseModel
+from ml_model.models import NeuronModel
class PaymentPlan(BaseModel):
@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
-from ml_model.models import BaseModel
+from core.models import BaseModel
class UserPaymentMethod(BaseModel):
@@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
from django.db import models
from django.utils.translation import gettext_lazy as _
-from ml_model.models import BaseModel
+from core.models import BaseModel
class ErrorReport(BaseModel):
@@ -1,7 +1,7 @@
from django.db import models
from authentication.models import CustomUserModel
-from ml_model.models import BaseModel
+from core.models import BaseModel
class RequestReponseLog(BaseModel):
@@ -10,7 +10,11 @@ class RequestReponseLog(BaseModel):
from_ip = models.GenericIPAddressField(verbose_name='От IP адреса')
request_body = models.TextField(null=True, blank=True, verbose_name='Тело запроса')
request_user = models.ForeignKey(
- CustomUserModel, verbose_name='От пользователя', on_delete=models.CASCADE, null=True, blank=True
+ CustomUserModel,
+ verbose_name='От пользователя',
+ on_delete=models.CASCADE,
+ null=True,
+ blank=True,
)
response_body = models.TextField(null=True, blank=True, verbose_name='Тело ответа')
status_code = models.IntegerField(verbose_name='Код ответа')
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.9 on 2024-12-22 13:19
+# Generated by Django 5.0.9 on 2025-01-08 12:32
import django.db.models.deletion
import django_minio_backend.models
@@ -23,7 +23,7 @@ class Migration(migrations.Migration):
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')),
('output_content', models.TextField(blank=True, null=True, verbose_name='Исходящий промпт')),
- ('starred', models.BooleanField(default=False, verbose_name='Добавлено в избранное')),
+ ('favourite', models.BooleanField(default=False, verbose_name='В избранном')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Когда создано')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='user_copywrites', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
@@ -63,6 +63,7 @@ class Migration(migrations.Migration):
('order', models.PositiveIntegerField(db_index=True, editable=False, verbose_name='order')),
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=50, unique=True, verbose_name='Название')),
+ ('slug', models.SlugField(unique=True, verbose_name='Ярлык')),
('description', models.CharField(blank=True, max_length=200, null=True, verbose_name='Описание')),
('picture', models.FileField(blank=True, null=True, storage=django_minio_backend.models.MinioBackend(bucket_name='air-templates-pictures'), upload_to=tools.copywrite.models.template_picture_uploader, verbose_name='Картинка')),
('content', models.TextField(help_text='Синтаксис в этом поле зависит от Движка шаблонов', verbose_name='Контент')),
@@ -83,7 +84,7 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=100, verbose_name='Название')),
('sysname', models.CharField(max_length=100, verbose_name='Системное название')),
('default_value', models.JSONField(blank=True, null=True, verbose_name='Стандартное значение')),
- ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='template_variables', to='copywrite.template', verbose_name='Шаблон')),
+ ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='template_variables', to='copywrite.template', to_field='slug', verbose_name='Шаблон')),
],
options={
'verbose_name': 'Переменная шаблона',
@@ -0,0 +1,23 @@
+# Generated by Django 5.0.9 on 2025-01-08 13:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('copywrite', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='templatecategory',
+ options={'ordering': ('order',), 'verbose_name': 'Категория', 'verbose_name_plural': 'Категории'},
+ ),
+ migrations.AddField(
+ model_name='templatecategory',
+ name='order',
+ field=models.PositiveIntegerField(db_index=True, default=0, editable=False, verbose_name='order'),
+ preserve_default=False,
+ ),
+ ]
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.9 on 2025-01-08 15:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('copywrite', '0002_alter_templatecategory_options_and_more'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='templatecategory',
+ name='slug',
+ field=models.CharField(max_length=50, unique=True, verbose_name='Ярлык'),
+ ),
+ ]
@@ -0,0 +1,19 @@
+# Generated by Django 5.0.9 on 2025-01-08 15:34
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('copywrite', '0003_alter_templatecategory_slug'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='template',
+ name='category',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='templates_categories', to='copywrite.templatecategory', to_field='slug', verbose_name='Категория'),
+ ),
+ ]
@@ -0,0 +1,22 @@
+# Generated by Django 5.0.9 on 2025-01-08 19:38
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('copywrite', '0004_alter_template_category'),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name='templatevariable',
+ options={'ordering': ('order',), 'verbose_name': 'Переменная шаблона', 'verbose_name_plural': 'Переменные шаблонов'},
+ ),
+ migrations.AddField(
+ model_name='template',
+ name='hidden',
+ field=models.BooleanField(default=True, verbose_name='Скрыт'),
+ ),
+ ]
@@ -1,4 +1,4 @@
-# Generated by Django 5.0.9 on 2024-12-22 16:42
+# Generated by Django 5.0.9 on 2025-01-08 20:39
from django.db import migrations, models
@@ -6,17 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
- ('copywrite', '0001_initial'),
+ ('copywrite', '0005_alter_templatevariable_options_template_hidden'),
]
operations = [
- migrations.RemoveField(
- model_name='copywrite',
- name='starred',
- ),
migrations.AddField(
model_name='copywrite',
- name='favourite',
- field=models.BooleanField(default=False, verbose_name='В избранном'),
+ name='deleted',
+ field=models.BooleanField(default=False, verbose_name='Удален'),
),
]
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.9 on 2025-01-08 20:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('copywrite', '0006_copywrite_deleted'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='templatevariable',
+ name='required',
+ field=models.BooleanField(default=False, verbose_name='Обязательный'),
+ ),
+ ]
@@ -1,4 +1,4 @@
-from typing import List, Literal
+from typing import List
from uuid import UUID
from channels.routing import URLRouter
@@ -10,6 +10,7 @@ from tools.copywrite.consumers import CopywriteConsumer
from tools.copywrite.exceptions import CopywriteAlreadyGenerated
from tools.copywrite.schemas import (
CopywriteFilterSchema,
+ CopywriteQuery,
CreateDraftSchema,
CreateOverridenVariableSchema,
OverridenVariableSchema,
@@ -26,8 +27,7 @@ from tools.copywrite.schemas import (
)
from tools.copywrite.services.copywrite import CopywriteService
from tools.copywrite.services.template import TemplateService
-
-from .tasks import generate as generate_task
+from tools.copywrite.tasks import generate as generate_task
router = Router(tags=['copywrite'], auth=SyncAuthBearer())
@@ -48,11 +48,13 @@ ws_router = URLRouter(
)
def list_copywrites(
request,
+ ordering: Query[CopywriteQuery],
filters: CopywriteFilterSchema = Query(...),
- ordering: Literal['favourite'] = 'favourite',
):
return CopywriteService.list_copywrites_by_user(
- user=request.auth, filters=filters.get_filter_expression(), ordering=ordering
+ user=request.auth,
+ filters=filters.get_filter_expression(),
+ **ordering.model_dump(),
)
@@ -82,16 +84,30 @@ def generate(request, id: UUID):
generate_task.delay(copywrite_id=id)
-@router.put('copywrites/{id}/', tags=['copywrite/copywrites'], response={204: None})
+@router.put(
+ 'copywrites/{id}/favourite', tags=['copywrite/copywrites'], response={204: None}
+)
def mark_as_favourite(request, id: UUID):
return CopywriteService.mark_as_favourite(copywrite_id=id)
+@router.delete(
+ 'copywrites/{id}/favourite', tags=['copywrite/copywrites'], response={204: None}
+)
+def remove_from_favourite(request, id: UUID):
+ return CopywriteService.remove_from_favourite(copywrite_id=id)
+
+
@router.patch('copywrites/{id}/', tags=['copywrite/copywrites'], response={204: None})
def update_input_content(request, id: UUID, data: UpdateSelfCopywriteSchema):
return CopywriteService.update_input_content(copywrite_id=id, **data.model_dump())
+@router.delete('copywrites/{id}/', tags=['copywrite/copywrites'], response={204: None})
+def delete_copywrite(request, id: UUID):
+ return CopywriteService.delete(copywrite_id=id)
+
+
@router.post(
'copywrites/{copywrite_id}/variables/',
tags=['copywrite/variables'],
@@ -1,4 +1,4 @@
-from typing import Any, Generator, Literal
+from typing import Any, Generator, List, Literal
from uuid import UUID
from django.contrib.contenttypes.models import ContentType
@@ -22,15 +22,17 @@ class CopywriteService:
cls,
user: CustomUserModel,
filters: Q = Q(),
- ordering: Literal['favourite'] = 'favourite',
+ sort: List[Literal['favourite', 'created_at', '-favourite', '-created_at']] = [
+ '-favourite',
+ '-created_at',
+ ],
) -> QuerySet[Copywrite]:
return (
Copywrite.objects.filter(user=user)
.filter(filters)
- .prefetch_related(
- 'polymorphic_ctype',
- )
- .order_by(ordering)
+ .filter(deleted=False)
+ .prefetch_related('polymorphic_ctype')
+ .order_by(*sort)
)
@classmethod
@@ -55,6 +57,10 @@ class CopywriteService:
def mark_as_favourite(cls, copywrite_id: UUID) -> None:
Copywrite.objects.filter(id=copywrite_id).update(favourite=True)
+ @classmethod
+ def remove_from_favourite(cls, copywrite_id: UUID):
+ Copywrite.objects.filter(id=copywrite_id).update(favourite=False)
+
@classmethod
def set_output(cls, copywrite_id: UUID, output_content: str) -> None:
Copywrite.objects.filter(id=copywrite_id).update(output_content=output_content)
@@ -94,7 +100,7 @@ class CopywriteService:
'polymorphic_ctype'
).get(id=copywrite_id)
if isinstance(cp, SelfCopywrite):
- content = cp.input_content or ''
+ content = str(cp.input_content) or ''
elif isinstance(cp, TemplateCopywrite):
overriden_vars = {
overriden_var.variable.id: overriden_var.value
@@ -106,8 +112,12 @@ class CopywriteService:
for var in cp.template.variables
if (active_val := overriden_vars.get(var.id) or var.default_value)
}
- content = blueprint.render(Context(ctx))
+ content = str(blueprint.render(Context(ctx)))
else:
raise NotImplementedError
for chunk in Chatgpt.evaluate(content=content, stream=True):
yield chunk
+
+ @classmethod
+ def delete(cls, copywrite_id: UUID) -> None:
+ Copywrite.objects.filter(id=copywrite_id).update(deleted=True)
@@ -8,7 +8,7 @@ from tools.copywrite.models import Template, TemplateCategory
class TemplateService:
@classmethod
def list_templates(cls, filters: Q = Q()) -> QuerySet[Template]:
- return Template.objects.filter(filters)
+ return Template.objects.filter(filters).filter(hidden=False)
@classmethod
def get_template_by_id(cls, template_id: UUID) -> Template:
@@ -1,10 +1,17 @@
from django.contrib import admin
+from import_export.admin import ImportExportMixin
+from ordered_model.admin import (
+ OrderedInlineModelAdminMixin,
+ OrderedModelAdmin,
+ OrderedTabularInline,
+)
from polymorphic.admin import (
PolymorphicChildModelAdmin,
PolymorphicChildModelFilter,
PolymorphicParentModelAdmin,
)
+from ml_model.inlines import ModelConfigurationInline
from tools.copywrite.models import (
Copywrite,
OverridenVariable,
@@ -14,39 +21,68 @@ from tools.copywrite.models import (
TemplateCopywrite,
TemplateVariable,
)
+from tools.copywrite.resources import (
+ TemplateCategoryResource,
+ TemplateResource,
+ TemplateVariableResource,
+)
-class CopywriteChildAdmin(PolymorphicChildModelAdmin): ...
+class CopywriteChildAdmin(PolymorphicChildModelAdmin):
+ inlines = (ModelConfigurationInline,)
@admin.register(SelfCopywrite)
-class SelfCopywriteAdmin(CopywriteChildAdmin): ...
+class SelfCopywriteAdmin(CopywriteChildAdmin):
+ inlines = (ModelConfigurationInline,)
class OverridenVariableInline(admin.TabularInline):
model = OverridenVariable
+ extra = 0
+ can_delete = False
+ show_change_link = False
@admin.register(TemplateCopywrite)
class TemplateCopywriteAdmin(CopywriteChildAdmin):
- inlines = (OverridenVariableInline,)
+ inlines = (OverridenVariableInline, ModelConfigurationInline)
@admin.register(Copywrite)
class CopywriteAdmin(PolymorphicParentModelAdmin):
+ list_display = ('user', 'created_at', 'favourite', '_draft')
child_models = (SelfCopywrite, TemplateCopywrite)
list_filter = (PolymorphicChildModelFilter,)
+ @admin.display(description='Черновик', boolean=True)
+ def _draft(self, obj: Copywrite):
+ return obj.draft
+
@admin.register(TemplateCategory)
-class TemplateCategoryAdmin(admin.ModelAdmin): ...
+class TemplateCategoryAdmin(ImportExportMixin, OrderedModelAdmin):
+ list_display = ('title', 'slug', 'move_up_down_links')
+ resource_classes = (TemplateCategoryResource,)
-class TemplateVariableInline(admin.TabularInline):
+class TemplateVariableInline(OrderedTabularInline):
model = TemplateVariable
+ fields = ('name', 'sysname', 'default_value', 'move_up_down_links')
+ readonly_fields = ('move_up_down_links',)
+ extra = 1
+ can_delete = False
+ show_change_link = False
@admin.register(Template)
-class TemplateAdmin(admin.ModelAdmin):
- list_display = ('id', 'title', 'category')
- inlines = (TemplateVariableInline,)
+class TemplateAdmin(OrderedInlineModelAdminMixin, ImportExportMixin, OrderedModelAdmin):
+ list_display = ('id', 'title', 'category', 'hidden', 'move_up_down_links')
+ inlines = (TemplateVariableInline, ModelConfigurationInline)
+ list_filter = ('category__title',)
+ resource_classes = (TemplateResource,)
+
+
+@admin.register(TemplateVariable)
+class TemplateVariableAdmin(ImportExportMixin, admin.ModelAdmin):
+ resource_classes = (TemplateVariableResource,)
@@ -1 +1 @@
-COPYWRITE_WS_KEY = 'copywrite_%s'
+COPYWRITE_WS_KEY = 'copywrite.%s'
@@ -2,6 +2,7 @@ from logging import getLogger
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from channels_redis.core import RedisChannelLayer
+from django.core.cache import cache
from tools.copywrite.constants import COPYWRITE_WS_KEY
@@ -12,11 +13,14 @@ class CopywriteConsumer(AsyncJsonWebsocketConsumer):
channel_layer: RedisChannelLayer
async def connect(self):
+ copywrite_id = f"{self.scope['url_route']['kwargs']['copywrite_id']}"
await self.channel_layer.group_add(
- COPYWRITE_WS_KEY % f"{self.scope['url_route']['kwargs']['copywrite_id']}",
+ COPYWRITE_WS_KEY % copywrite_id,
self.channel_name,
)
- return await self.accept()
+ await self.accept()
+ if content := cache.get(COPYWRITE_WS_KEY % copywrite_id):
+ await self.send_json({'chunk': content, 'end': False})
async def close(self, code=None, reason=None):
await self.channel_layer.group_discard(
@@ -2,20 +2,23 @@ from typing import Optional
from uuid import uuid4
from django.contrib.auth import get_user_model
+from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.db.models import QuerySet
from django_minio_backend import MinioBackend
from ordered_model.models import OrderedModel
from polymorphic.models import PolymorphicManager, PolymorphicModel
+from ml_model.models import ModelConfiguration
+
def template_picture_uploader(instance: 'Template', filename: str):
return f'{instance.title}/{filename}'
-class TemplateCategory(models.Model):
+class TemplateCategory(OrderedModel):
title = models.CharField(max_length=50, default='Общее', verbose_name='Название')
- slug = models.CharField(max_length=50, verbose_name='Ярлык')
+ slug = models.CharField(unique=True, max_length=50, verbose_name='Ярлык')
def __str__(self) -> str:
return self.title
@@ -24,7 +27,7 @@ class TemplateCategory(models.Model):
def templates(self) -> QuerySet['Template']:
return self.templates_categories.all()
- class Meta:
+ class Meta(OrderedModel.Meta):
verbose_name = 'Категория'
verbose_name_plural = 'Категории'
@@ -35,6 +38,7 @@ class Template(OrderedModel):
)
title = models.CharField(max_length=50, unique=True, verbose_name='Название')
+ slug = models.SlugField(unique=True, verbose_name='Ярлык')
description = models.CharField(
null=True, blank=True, max_length=200, verbose_name='Описание'
)
@@ -48,9 +52,18 @@ class Template(OrderedModel):
category = models.ForeignKey(
TemplateCategory,
on_delete=models.PROTECT,
+ to_field='slug',
verbose_name='Категория',
related_name='templates_categories',
)
+ configuration = GenericRelation(
+ ModelConfiguration,
+ object_id_field='oid',
+ content_type_field='ct',
+ null=True,
+ blank=True,
+ )
+ hidden = models.BooleanField(default=True, verbose_name='Скрыт')
content = models.TextField(
verbose_name='Контент',
@@ -80,6 +93,7 @@ class TemplateVariable(OrderedModel):
template = models.ForeignKey(
Template,
+ to_field='slug',
on_delete=models.CASCADE,
verbose_name='Шаблон',
related_name='template_variables',
@@ -87,13 +101,16 @@ class TemplateVariable(OrderedModel):
name = models.CharField(max_length=100, verbose_name='Название')
sysname = models.CharField(max_length=100, verbose_name='Системное название')
+ required = models.BooleanField(default=False, verbose_name='Обязательный')
default_value = models.JSONField(
null=True, blank=True, verbose_name='Стандартное значение'
)
+ order_with_respect_to = 'template'
+
class Meta(OrderedModel.Meta):
verbose_name = 'Переменная шаблона'
- verbose_name_plural = 'Переменные шаблона'
+ verbose_name_plural = 'Переменные шаблонов'
class Copywrite(PolymorphicModel):
@@ -113,6 +130,14 @@ class Copywrite(PolymorphicModel):
)
favourite = models.BooleanField(default=False, verbose_name='В избранном')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='Когда создано')
+ deleted = models.BooleanField(default=False, verbose_name='Удален')
+ configuration = GenericRelation(
+ ModelConfiguration,
+ object_id_field='oid',
+ content_type_field='ct',
+ null=True,
+ blank=True,
+ )
objects = PolymorphicManager()
@@ -0,0 +1,32 @@
+from import_export import fields as ie_fields
+from import_export.resources import ModelResource
+from import_export.widgets import ForeignKeyWidget
+
+from tools.copywrite.models import Template, TemplateCategory, TemplateVariable
+
+
+class TemplateCategoryResource(ModelResource):
+ class Meta:
+ model = TemplateCategory
+ exclude = ('id', 'order')
+ import_id_fields = ('slug',)
+
+
+class TemplateResource(ModelResource):
+ category = ie_fields.Field(
+ column_name='category',
+ attribute='category',
+ widget=ForeignKeyWidget(TemplateCategory, 'slug'),
+ )
+
+ class Meta:
+ model = Template
+ exclude = ('id', 'order', 'picture')
+ import_id_fields = ('slug', 'category')
+
+
+class TemplateVariableResource(ModelResource):
+ class Meta:
+ model = TemplateVariable
+ exclude = ('id', 'order')
+ import_id_fields = ('sysname', 'template')
@@ -4,6 +4,7 @@ from uuid import UUID
from django.db.models import Q
from ninja import Field, FilterSchema, ModelSchema, Schema
+from ml_model.schemas import ModelConfigurationSchema
from tools.copywrite.models import (
OverridenVariable,
SelfCopywrite,
@@ -30,6 +31,8 @@ class UpdateSelfCopywriteSchema(ModelSchema):
'polymorphic_ctype',
'output_content',
'user',
+ 'deleted',
+ 'configuration',
)
fields_optional = '__all__'
@@ -72,7 +75,14 @@ class TemplateSchema(ModelSchema):
class Meta:
model = Template
- exclude = ('order', 'content', 'category', 'description')
+ exclude = (
+ 'order',
+ 'content',
+ 'category',
+ 'description',
+ 'configuration',
+ 'hidden',
+ )
class TemplateFilterSchema(FilterSchema):
@@ -84,17 +94,11 @@ class BaseCopywriteSchema:
class SelfCopywriteSchema(BaseCopywriteSchema, ModelSchema):
- draft: bool
- label: Optional[str] = None
+ configuration: List[ModelConfigurationSchema]
class Meta:
model = SelfCopywrite
- exclude = (
- 'copywrite_ptr',
- 'user',
- 'polymorphic_ctype',
- 'created_at',
- )
+ exclude = ('copywrite_ptr', 'user', 'polymorphic_ctype', 'created_at', 'deleted')
class SelfCopywriteShortSchema(BaseCopywriteSchema, ModelSchema):
@@ -130,17 +134,11 @@ class CreateOverridenVariableSchema(ModelSchema):
class TemplateCopywriteSchema(BaseCopywriteSchema, ModelSchema):
template: TemplateSchema
overriden_variables: List[OverridenVariableSchema]
- draft: bool
- label: Optional[str] = None
+ configuration: List[ModelConfigurationSchema] = []
class Meta:
model = TemplateCopywrite
- exclude = (
- 'copywrite_ptr',
- 'user',
- 'polymorphic_ctype',
- 'created_at',
- )
+ exclude = ('copywrite_ptr', 'user', 'polymorphic_ctype', 'created_at', 'deleted')
class TemplateCopywriteShortSchema(BaseCopywriteSchema, ModelSchema):
@@ -174,3 +172,10 @@ class UpdateOverridenVariableSchema(ModelSchema):
class Meta:
model = OverridenVariable
fields = ('value',)
+
+
+class CopywriteQuery(Schema):
+ sort: List[Literal['favourite', 'created_at', '-favourite', '-created_at']] = [
+ '-favourite',
+ '-created_at',
+ ]
@@ -0,0 +1,13 @@
+from typing import Type
+
+from django.db.models.signals import pre_save
+from django.dispatch import receiver
+
+from tools.copywrite.models import TemplateCopywrite
+
+
+@receiver(pre_save, sender=TemplateCopywrite)
+def copy_model_configuration(
+ sender: Type[TemplateCopywrite], instance: TemplateCopywrite, **kwargs
+):
+ print(kwargs)
@@ -4,6 +4,7 @@ from asgiref.sync import async_to_sync
from celery import shared_task
from channels.layers import get_channel_layer
from channels_redis.core import RedisChannelLayer
+from django.core.cache import cache
from tools.copywrite.constants import COPYWRITE_WS_KEY
from tools.copywrite.services.copywrite import CopywriteService
@@ -19,8 +20,10 @@ def generate(copywrite_id: UUID):
{'type': 'generate.chunk', 'event_data': {'chunk': chunk, 'end': False}},
)
content += chunk
+ cache.set(f'copywrite:{copywrite_id}', content)
async_to_sync(layer.group_send)(
COPYWRITE_WS_KEY % copywrite_id,
{'type': 'generate.chunk', 'event_data': {'chunk': '', 'end': True}},
)
CopywriteService.set_output(copywrite_id=copywrite_id, output_content=content)
+ cache.delete(f'copywrite:{copywrite_id}')
@@ -1,4 +1,5 @@
from django.apps import AppConfig
+from django.core.signals import setting_changed
from django.utils.translation import gettext_lazy as _
@@ -19,6 +20,11 @@ class CopywriteConfig(AppConfig):
name = 'tools.copywrite'
verbose_name = _('Copywrite')
+ def ready(self):
+ from tools.copywrite import signals as copywrite_signals
+
+ setting_changed.connect(copywrite_signals.copy_model_configuration)
+
class PublicAPIConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
@@ -3,7 +3,7 @@ FROM python:3.12-slim as build
WORKDIR /code
COPY pyproject.toml poetry.lock /code/
-RUN --mount=type=cache,target=/root/.cache/pip pip install poetry
+RUN --mount=type=cache,target=/root/.cache/pip pip install poetry && poetry self add poetry-plugin-export
RUN poetry export --only main --output=requirements.txt
FROM python:3.12-slim
@@ -3,7 +3,7 @@ FROM python:3.12-slim as build
WORKDIR /code
COPY pyproject.toml poetry.lock /code/
-RUN --mount=type=cache,target=/root/.cache/pip pip install poetry
+RUN --mount=type=cache,target=/root/.cache/pip pip install poetry && poetry self add poetry-plugin-export
RUN poetry export --with test --with debug --output=requirements.txt
@@ -17,6 +17,7 @@ djangorestframework-simplejwt = "^5.2.2"
djangorestframework = "^3.14.0"
yookassa = "^2.4.0"
environs = "^9.5.0"
+gunicorn = {extras = ["gevent"], version = "^23.0.0"}
dj-rest-auth = "^4.0.1"
django-celery-beat = "^2.5.0"
minio = "^7.1.15"
@@ -56,6 +57,7 @@ httptools = "^0.6.4"
uvloop = "^0.21.0"
wsproto = "^1.2.0"
channels-redis = "^4.2.1"
+tiktoken = "<0.6.0"
[tool.poetry.group.test.dependencies]