feat(products): добавить генератор названий букетов с ai и тесты
- Добавить модуль ai с генератором названий букетов - Обновить __init__.py для экспорта нового сервиса - Добавить тесты для проверки работы генератора
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Сервисы для бизнес-логики products приложения.
|
Сервисы для бизнес-логики products приложения.
|
||||||
Следует принципу "Skinny Models, Fat Services".
|
Следует принципу "Тонкие модели, толстые сервисы".
|
||||||
"""
|
"""
|
||||||
from .unit_service import UnitOfMeasureService
|
from .unit_service import UnitOfMeasureService
|
||||||
|
from .ai.bouquet_names import BouquetNameGenerator
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'UnitOfMeasureService',
|
'UnitOfMeasureService',
|
||||||
|
'BouquetNameGenerator',
|
||||||
]
|
]
|
||||||
|
|||||||
6
myproject/products/services/ai/__init__.py
Normal file
6
myproject/products/services/ai/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
"""
|
||||||
|
AI-сервисы для products приложения.
|
||||||
|
|
||||||
|
Содержит инструменты для взаимодействия с нейросетями для решения специфичных
|
||||||
|
бизнес-задач, таких как генерация названий продуктов, описаний, классификация и т.д.
|
||||||
|
"""
|
||||||
48
myproject/products/services/ai/base.py
Normal file
48
myproject/products/services/ai/base.py
Normal 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
|
||||||
130
myproject/products/services/ai/bouquet_names.py
Normal file
130
myproject/products/services/ai/bouquet_names.py
Normal 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
|
||||||
169
myproject/products/tests/test_ai_bouquet_names.py
Normal file
169
myproject/products/tests/test_ai_bouquet_names.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
from django_tenants.test.cases import TenantTestCase
|
||||||
|
from products.services import BouquetNameGenerator
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
class BouquetNameGeneratorTestCase(TenantTestCase):
|
||||||
|
"""
|
||||||
|
Тесты для сервиса генерации названий букетов
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Создаем экземпляр сервиса для тестирования
|
||||||
|
"""
|
||||||
|
self.generator = BouquetNameGenerator()
|
||||||
|
|
||||||
|
@patch('products.services.ai.bouquet_names.BouquetNameGenerator.get_glm_service')
|
||||||
|
def test_generate_with_mock_glm(self, mock_get_glm_service):
|
||||||
|
"""
|
||||||
|
Тест генерации названий с мок-объектом GLM сервиса
|
||||||
|
"""
|
||||||
|
# Создаем мок-объект сервиса
|
||||||
|
mock_service = MagicMock()
|
||||||
|
mock_service.generate_text.return_value = (
|
||||||
|
True,
|
||||||
|
"Текст успешно сгенерирован",
|
||||||
|
{
|
||||||
|
'generated_text': (
|
||||||
|
"1. Розавая мечта\n"
|
||||||
|
"2. Лиловые настроения\n"
|
||||||
|
"3. Яркий букет для дня рождения\n"
|
||||||
|
"4. Сладкий сюрприз\n"
|
||||||
|
"5. Романтическое вдохновение"
|
||||||
|
),
|
||||||
|
'model': 'glm-4',
|
||||||
|
'usage': {'prompt_tokens': 100, 'completion_tokens': 50}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mock_get_glm_service.return_value = mock_service
|
||||||
|
|
||||||
|
# Вызываем метод генерации
|
||||||
|
success, msg, data = self.generator.generate(count=5)
|
||||||
|
|
||||||
|
# Проверки
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertIn("Сгенерировано 5 названий для букетов", msg)
|
||||||
|
self.assertIsNotNone(data)
|
||||||
|
self.assertIn('names', data)
|
||||||
|
self.assertEqual(len(data['names']), 5)
|
||||||
|
self.assertEqual(data['model'], 'glm-4')
|
||||||
|
self.assertIn('usage', data)
|
||||||
|
|
||||||
|
# Проверяем, что названия содержат нужные слова
|
||||||
|
expected_names = [
|
||||||
|
"Розавая мечта",
|
||||||
|
"Лиловые настроения",
|
||||||
|
"Яркий букет для дня рождения",
|
||||||
|
"Сладкий сюрприз",
|
||||||
|
"Романтическое вдохновение"
|
||||||
|
]
|
||||||
|
self.assertEqual(data['names'], expected_names)
|
||||||
|
|
||||||
|
@patch('products.services.ai.bouquet_names.BouquetNameGenerator.get_glm_service')
|
||||||
|
@patch('products.services.ai.bouquet_names.BouquetNameGenerator.get_openrouter_service')
|
||||||
|
def test_no_active_integration(self, mock_get_openrouter, mock_get_glm):
|
||||||
|
"""
|
||||||
|
Тест случая, когда нет активных интеграций
|
||||||
|
"""
|
||||||
|
mock_get_glm.return_value = None
|
||||||
|
mock_get_openrouter.return_value = None
|
||||||
|
|
||||||
|
success, msg, data = self.generator.generate(count=10)
|
||||||
|
|
||||||
|
self.assertFalse(success)
|
||||||
|
self.assertEqual(msg, "Нет активных AI-интеграций")
|
||||||
|
self.assertIsNone(data)
|
||||||
|
|
||||||
|
@patch('products.services.ai.bouquet_names.BouquetNameGenerator.get_glm_service')
|
||||||
|
def test_generate_with_characteristics(self, mock_get_glm_service):
|
||||||
|
"""
|
||||||
|
Тест генерации с характеристиками
|
||||||
|
"""
|
||||||
|
# Создаем мок-объект сервиса
|
||||||
|
mock_service = MagicMock()
|
||||||
|
mock_service.generate_text.return_value = (
|
||||||
|
True,
|
||||||
|
"Текст успешно сгенерирован",
|
||||||
|
{
|
||||||
|
'generated_text': (
|
||||||
|
"1. Ромашковое небо\n"
|
||||||
|
"2. Лавандовый спокойствие\n"
|
||||||
|
"3. Свежие ароматы\n"
|
||||||
|
"4. Милая композиция\n"
|
||||||
|
"5. Нежный букет"
|
||||||
|
),
|
||||||
|
'model': 'glm-4',
|
||||||
|
'usage': {'prompt_tokens': 120, 'completion_tokens': 45}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mock_get_glm_service.return_value = mock_service
|
||||||
|
|
||||||
|
success, msg, data = self.generator.generate(
|
||||||
|
count=5,
|
||||||
|
characteristics="ромашки, лаванда, свежие",
|
||||||
|
occasion="день матери"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(success)
|
||||||
|
self.assertIn("Сгенерировано 5 названий для букетов", msg)
|
||||||
|
self.assertEqual(len(data['names']), 5)
|
||||||
|
# Проверяем, что сервис был вызван с нужными параметрами
|
||||||
|
mock_service.generate_text.assert_called_once()
|
||||||
|
|
||||||
|
def test_parse_response_with_markdown(self):
|
||||||
|
"""
|
||||||
|
Тест парсинга ответа с Markdown форматированием
|
||||||
|
"""
|
||||||
|
response_text = """
|
||||||
|
Here are 3 beautiful bouquet names for you:
|
||||||
|
|
||||||
|
1. **Spring Blossom Delight**
|
||||||
|
2. *Romantic Rose Elegance*
|
||||||
|
3. "Sunny Daisy Joy"
|
||||||
|
|
||||||
|
I hope you love these!
|
||||||
|
"""
|
||||||
|
|
||||||
|
names = self.generator._parse_response(response_text)
|
||||||
|
self.assertEqual(len(names), 3)
|
||||||
|
self.assertEqual(names[0], "Spring Blossom Delight")
|
||||||
|
self.assertEqual(names[1], "Romantic Rose Elegance")
|
||||||
|
self.assertEqual(names[2], "Sunny Daisy Joy")
|
||||||
|
|
||||||
|
def test_parse_response_with_duplicates(self):
|
||||||
|
"""
|
||||||
|
Тест парсинга ответа с дубликатами
|
||||||
|
"""
|
||||||
|
response_text = """
|
||||||
|
1. Розавая мечта
|
||||||
|
2. Лиловые настроения
|
||||||
|
3. Розавая мечта
|
||||||
|
4. Сладкий сюрприз
|
||||||
|
5. Лиловые настроения
|
||||||
|
"""
|
||||||
|
|
||||||
|
names = self.generator._parse_response(response_text)
|
||||||
|
self.assertEqual(len(names), 3)
|
||||||
|
self.assertIn("Розавая мечта", names)
|
||||||
|
self.assertIn("Лиловые настроения", names)
|
||||||
|
self.assertIn("Сладкий сюрприз", names)
|
||||||
|
|
||||||
|
def test_parse_response_empty(self):
|
||||||
|
"""
|
||||||
|
Тест парсинга пустого ответа
|
||||||
|
"""
|
||||||
|
response_text = """
|
||||||
|
"""
|
||||||
|
names = self.generator._parse_response(response_text)
|
||||||
|
self.assertEqual(len(names), 0)
|
||||||
|
|
||||||
|
def test_parse_response_no_names(self):
|
||||||
|
"""
|
||||||
|
Тест парсинга ответа без названий
|
||||||
|
"""
|
||||||
|
response_text = """
|
||||||
|
I'm sorry, but I can't help with that right now.
|
||||||
|
"""
|
||||||
|
names = self.generator._parse_response(response_text)
|
||||||
|
self.assertEqual(len(names), 0)
|
||||||
Reference in New Issue
Block a user