feat(products): добавить генератор названий букетов с ai и тесты

- Добавить модуль ai с генератором названий букетов
- Обновить __init__.py для экспорта нового сервиса
- Добавить тесты для проверки работы генератора
This commit is contained in:
2026-01-22 12:12:57 +03:00
parent 036b9d1634
commit 01873be15d
5 changed files with 356 additions and 1 deletions

View File

@@ -0,0 +1,6 @@
"""
AI-сервисы для products приложения.
Содержит инструменты для взаимодействия с нейросетями для решения специфичных
бизнес-задач, таких как генерация названий продуктов, описаний, классификация и т.д.
"""

View File

@@ -0,0 +1,48 @@
from abc import ABC, abstractmethod
from typing import Tuple, Optional, Dict
from integrations.services.ai_services.glm_service import GLMIntegrationService
from integrations.services.ai_services.openrouter_service import OpenRouterIntegrationService
from integrations.models.ai_services.glm import GLMIntegration
from integrations.models.ai_services.openrouter import OpenRouterIntegration
import logging
logger = logging.getLogger(__name__)
class BaseAIProductService(ABC):
"""
Абстрактный базовый класс для AI-сервисов продуктов
"""
@abstractmethod
def generate(self, **kwargs) -> Tuple[bool, str, Optional[Dict]]:
"""
Основной метод генерации
"""
pass
@classmethod
def get_glm_service(cls) -> Optional[GLMIntegrationService]:
"""
Получить сервис GLM из активной интеграции
"""
try:
integration = GLMIntegration.objects.filter(is_active=True).first()
if integration:
return GLMIntegrationService(integration)
except Exception as e:
logger.error(f"Ошибка при получении GLM сервиса: {str(e)}")
return None
@classmethod
def get_openrouter_service(cls) -> Optional[OpenRouterIntegrationService]:
"""
Получить сервис OpenRouter из активной интеграции
"""
try:
integration = OpenRouterIntegration.objects.filter(is_active=True).first()
if integration:
return OpenRouterIntegrationService(integration)
except Exception as e:
logger.error(f"Ошибка при получении OpenRouter сервиса: {str(e)}")
return None

View File

@@ -0,0 +1,130 @@
from typing import Tuple, Optional, Dict, List
from .base import BaseAIProductService
import logging
logger = logging.getLogger(__name__)
class BouquetNameGenerator(BaseAIProductService):
"""
Сервис для генерации названий букетов с помощью AI
"""
DEFAULT_SYSTEM_PROMPT = (
"Вы эксперт в создании красивых, привлекательных и продаваемых названий для букетов цветов. "
"Ваша цель — генерировать запоминающиеся и выразительные названия, которые привлекут покупателей. "
"Названия должны быть краткими (2-4 слов), креативными и соответствующими характеристикам букета. "
"Избегайте общих терминов. Фокусируйтесь на эмоциях, эстетике"
)
def generate(
self,
count: int = 500,
characteristics: Optional[str] = None,
occasion: Optional[str] = None,
language: str = "russian"
) -> Tuple[bool, str, Optional[Dict]]:
"""
Генерация названий букетов
Args:
count: Количество названий для генерации
characteristics: Характеристики букетов (например, "розы, лилии, яркий")
occasion: П'occasion (например, "день рождения, Valentine's Day")
language: Язык генерации
Returns:
Tuple: (success, message, data) где data содержит список названий
"""
logger.info(f"Генерация {count} названий для букетов")
# Получаем доступный AI-сервис
glm_service = self.get_glm_service()
if not glm_service:
openrouter_service = self.get_openrouter_service()
if not openrouter_service:
return False, "Нет активных AI-интеграций", None
service = openrouter_service
else:
service = glm_service
# Формируем промпт
prompt = f"Сгенерируй {count} креативных и привлекательных названий для букетов цветов"
if characteristics:
prompt += f" с следующими характеристиками: {characteristics}"
if occasion:
prompt += f" для праздника: {occasion}"
prompt += (
"\n\nТребования к каждому названию:\n"
"- Точно 2-4 слова\n"
"- Выразительные и эмоциональные\n"
"- Продаваемые и запоминающиеся\n"
"- Избегайте общих названий типа 'Букет #1'\n"
"- Фокусируйтесь на красоте, романтике и подарках\n"
"\nВерните названия в виде нумерованного списка, по одному на строку."
)
# Вызов AI-сервиса
success, msg, response = service.generate_text(
prompt=prompt,
system_prompt=self.DEFAULT_SYSTEM_PROMPT,
max_tokens=3000 # Увеличиваем лимит для большего числа названий
)
if not success:
return False, msg, None
# Парсим результат
names = self._parse_response(response.get('generated_text', ''))
return True, f"Сгенерировано {len(names)} названий для букетов", {
'names': names,
'model': response.get('model'),
'usage': response.get('usage')
}
def _parse_response(self, text: str) -> List[str]:
"""
Парсит текстовый ответ AI и извлекает названия букетов
"""
names = []
lines = text.split('\n')
for line in lines:
line = line.strip()
# Пропускаем пустые строки и заголовки
if not line or line.lower().startswith('here') or line.lower().startswith('names') or \
line.lower().startswith('i\'m') or line.lower().startswith('sorry') or \
line.lower().startswith('i hope') or line.lower().startswith('hope'):
continue
# Удаляем номера списка
if line and (line[0].isdigit() or line[0] == '-'):
# Удаляем номер и точку или дефис
if '.' in line:
line = line.split('.', 1)[1].strip()
else:
line = line[1:].strip()
# Пропускаем строки, которые стали пустыми после удаления номера
if not line:
continue
# Удаляем markdown форматирование (жирный, курсив)
line = line.replace('**', '').replace('*', '').replace('"', '').replace("'", '').strip()
if line:
names.append(line)
# Удаляем дубликаты
unique_names = []
seen = set()
for name in names:
if name not in seen:
seen.add(name)
unique_names.append(name)
return unique_names