diff --git a/myproject/integrations/migrations/0002_glmintegration_and_more.py b/myproject/integrations/migrations/0002_glmintegration_and_more.py new file mode 100644 index 0000000..991d07c --- /dev/null +++ b/myproject/integrations/migrations/0002_glmintegration_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 5.0.10 on 2026-01-14 14:17 + +import integrations.fields +import integrations.models.ai_services.glm +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrations', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='GLMIntegration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_active', models.BooleanField(db_index=True, default=False, help_text='Глобальный тумблер включения интеграции', verbose_name='Активна')), + ('name', models.CharField(blank=True, default='', help_text='Произвольное название для удобства (опционально)', max_length=100, verbose_name='Название')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), + ('extra_config', models.JSONField(blank=True, default=dict, verbose_name='Доп. настройки')), + ('api_key', integrations.fields.EncryptedCharField(blank=True, help_text='Ключ для доступа к API GLM от Z.AI (шифруется в БД)', max_length=500, verbose_name='API ключ')), + ('api_url', models.URLField(default='https://api.z.ai/api/paas/v4', help_text='URL для обращения к API GLM (обычно https://api.z.ai/api/paas/v4)', max_length=500, verbose_name='URL API')), + ('model_name', models.CharField(default='glm-4.7', help_text='Название используемой модели GLM (например, glm-4.7)', max_length=100, verbose_name='Название модели')), + ('temperature', models.FloatField(default=0.7, help_text='Параметр температуры для генерации (0.0-1.0)', validators=[integrations.models.ai_services.glm.validate_temperature], verbose_name='Температура')), + ('is_coding_endpoint', models.BooleanField(default=False, help_text='Отметьте, если используете специальный эндпоинт для задач программирования', verbose_name='Использовать эндпоинт для кодинга')), + ], + options={ + 'verbose_name': 'Интеграция GLM', + 'verbose_name_plural': 'Интеграции GLM', + }, + ), + migrations.AlterField( + model_name='recommerceintegration', + name='integration_type', + field=models.CharField(choices=[('marketplace', 'Маркетплейс'), ('payment', 'Платёжная система'), ('shipping', 'Служба доставки'), ('ai_service', 'Сервис ИИ')], default='marketplace', editable=False, max_length=20), + ), + migrations.AlterField( + model_name='woocommerceintegration', + name='integration_type', + field=models.CharField(choices=[('marketplace', 'Маркетплейс'), ('payment', 'Платёжная система'), ('shipping', 'Служба доставки'), ('ai_service', 'Сервис ИИ')], default='marketplace', editable=False, max_length=20), + ), + ] diff --git a/myproject/integrations/migrations/0007_openrouter.py b/myproject/integrations/migrations/0007_openrouter.py new file mode 100644 index 0000000..c21aaf4 --- /dev/null +++ b/myproject/integrations/migrations/0007_openrouter.py @@ -0,0 +1,35 @@ +# Generated migration for OpenRouter integration + +import integrations.fields +import integrations.models.ai_services.openrouter +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrations', '0006_update_glm_model_to_glm4'), + ] + + operations = [ + migrations.CreateModel( + name='OpenRouterIntegration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_active', models.BooleanField(db_index=True, default=False, help_text='Глобальный тумблер включения интеграции', verbose_name='Активна')), + ('name', models.CharField(blank=True, default='', help_text='Произвольное название для удобства (опционально)', max_length=100, verbose_name='Название')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), + ('extra_config', models.JSONField(blank=True, default=dict, verbose_name='Доп. настройки')), + ('api_key', integrations.fields.EncryptedCharField(blank=True, help_text='Ключ для доступа к API OpenRouter (шифруется в БД)', max_length=500, verbose_name='API ключ')), + ('api_url', models.URLField(default='https://openrouter.ai/api/v1', help_text='URL для обращения к API OpenRouter (обычно https://openrouter.ai/api/v1)', max_length=500, verbose_name='URL API')), + ('model_name', models.CharField(default='xiaomi/mimo-v2-flash:free', help_text='Название используемой модели OpenRouter', max_length=100, verbose_name='Название модели')), + ('temperature', models.FloatField(default=0.7, help_text='Параметр температуры для генерации (0.0-2.0)', validators=[integrations.models.ai_services.openrouter.validate_temperature], verbose_name='Температура')), + ('max_tokens', models.IntegerField(default=1000, help_text='Максимальное количество токенов в ответе', verbose_name='Макс. токенов')), + ], + options={ + 'verbose_name': 'Интеграция OpenRouter', + 'verbose_name_plural': 'Интеграции OpenRouter', + }, + ), + ] diff --git a/myproject/integrations/migrations/0008_openrouter_temperature_choices.py b/myproject/integrations/migrations/0008_openrouter_temperature_choices.py new file mode 100644 index 0000000..865da03 --- /dev/null +++ b/myproject/integrations/migrations/0008_openrouter_temperature_choices.py @@ -0,0 +1,32 @@ +# Generated migration for OpenRouter temperature choices + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('integrations', '0007_openrouter'), + ] + + operations = [ + migrations.AlterField( + model_name='openrouterintegration', + name='temperature', + field=models.FloatField( + default=0.7, + choices=[ + (0.1, '0.1 - Очень консервативно'), + (0.3, '0.3 - Консервативно'), + (0.5, '0.5 - Умеренно'), + (0.7, '0.7 - Баланс (по умолчанию)'), + (1.0, '1.0 - Креативно'), + (1.5, '1.5 - Очень креативно'), + (2.0, '2.0 - Максимальная креативность'), + ], + help_text='Параметр температуры для генерации (0.0-2.0)', + validators=[], + verbose_name='Температура' + ), + ), + ] diff --git a/myproject/integrations/models/ai_services/__init__.py b/myproject/integrations/models/ai_services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/integrations/models/ai_services/openrouter.py b/myproject/integrations/models/ai_services/openrouter.py new file mode 100644 index 0000000..6d91960 --- /dev/null +++ b/myproject/integrations/models/ai_services/openrouter.py @@ -0,0 +1,94 @@ +from django.db import models +from integrations.models.base import BaseIntegration, IntegrationType +from django.core.exceptions import ValidationError +from integrations.fields import EncryptedCharField + + +def validate_temperature(value): + """Валидатор для температуры, принимает значения от 0 до 2""" + if value < 0 or value > 2: + raise ValidationError('Температура должна быть в диапазоне 0.0-2.0') + + +# Список доступных моделей OpenRouter (бесплатные) +OPENROUTER_MODEL_CHOICES = [ + ('xiaomi/mimo-v2-flash:free', 'Xiaomi MIMO v2 Flash (Бесплатная)'), + ('mistralai/devstral-2512:free', 'Mistral Devstral 2512 (Бесплатная)'), + ('z-ai/glm-4.5-air:free', 'Z.AI GLM-4.5 Air (Бесплатная)'), + ('qwen/qwen3-coder:free', 'Qwen 3 Coder (Бесплатная)'), +] + +# Предустановленные значения температуры +OPENROUTER_TEMPERATURE_CHOICES = [ + (0.1, '0.1 - Очень консервативно'), + (0.3, '0.3 - Консервативно'), + (0.5, '0.5 - Умеренно'), + (0.7, '0.7 - Баланс (по умолчанию)'), + (1.0, '1.0 - Креативно'), + (1.5, '1.5 - Очень креативно'), + (2.0, '2.0 - Максимальная креативность'), +] + + +class AIIntegration(BaseIntegration): + """ + Базовая модель для интеграций с ИИ-сервисами + """ + class Meta: + abstract = True + + +class OpenRouterIntegration(AIIntegration): + """ + Интеграция с OpenRouter.ai + """ + integration_type = IntegrationType.AI_SERVICE + + api_key = EncryptedCharField( + max_length=500, + blank=True, + verbose_name="API ключ", + help_text="Ключ для доступа к API OpenRouter (шифруется в БД)" + ) + + api_url = models.URLField( + max_length=500, + default="https://openrouter.ai/api/v1", + verbose_name="URL API", + help_text="URL для обращения к API OpenRouter (обычно https://openrouter.ai/api/v1)" + ) + + model_name = models.CharField( + max_length=100, + default="xiaomi/mimo-v2-flash:free", + choices=OPENROUTER_MODEL_CHOICES, + verbose_name="Название модели", + help_text="Название используемой модели OpenRouter" + ) + + temperature = models.FloatField( + default=0.7, + choices=OPENROUTER_TEMPERATURE_CHOICES, + validators=[validate_temperature], + verbose_name="Температура", + help_text="Параметр температуры для генерации (0.0-2.0)" + ) + + max_tokens = models.IntegerField( + default=1000, + verbose_name="Макс. токенов", + help_text="Максимальное количество токенов в ответе" + ) + + class Meta: + verbose_name = "Интеграция OpenRouter" + verbose_name_plural = "Интеграции OpenRouter" + + @property + def is_configured(self) -> bool: + return bool(self.api_key) + + def clean(self): + super().clean() + if self.temperature < 0 or self.temperature > 2: + raise ValidationError({'temperature': 'Температура должна быть в диапазоне 0.0-2.0'}) diff --git a/myproject/integrations/services/ai_services/__init__.py b/myproject/integrations/services/ai_services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/integrations/services/ai_services/config.py b/myproject/integrations/services/ai_services/config.py new file mode 100644 index 0000000..7b26f08 --- /dev/null +++ b/myproject/integrations/services/ai_services/config.py @@ -0,0 +1,165 @@ +from dataclasses import dataclass +from typing import Optional +from django.conf import settings +import os +import logging + +logger = logging.getLogger(__name__) + + +@dataclass +class AIConfig: + """ + Конфигурация для интеграции с ИИ-сервисами + """ + api_key: str + api_url: str + model_name: str + temperature: float + is_coding_endpoint: bool = False + max_tokens: int = 1000 + timeout: int = 30 + + @classmethod + def from_integration_model(cls, integration_model): + """ + Создать конфигурацию из модели интеграции + """ + logger.info(f"=== Создание конфигурации из модели интеграции ===") + logger.info(f"Тип модели: {type(integration_model)}") + logger.info(f"Атрибуты модели: {dir(integration_model)}") + + try: + api_key = integration_model.api_key + logger.info(f"API key (первые 10 символов): {api_key[:10] if api_key else 'None'}...") + except AttributeError as e: + logger.error(f"Ошибка при получении api_key: {str(e)}") + raise + + # Всегда используем общий эндпоинт + api_url = "https://api.z.ai/api/paas/v4" + logger.info(f"Используем общий endpoint: {api_url}") + + try: + model_name = integration_model.model_name + logger.info(f"Model name: {model_name}") + except AttributeError as e: + logger.error(f"Ошибка при получении model_name: {str(e)}") + raise + + try: + temperature = float(integration_model.temperature) + logger.info(f"Temperature: {temperature}") + except AttributeError as e: + logger.error(f"Ошибка при получении temperature: {str(e)}") + raise + + return cls( + api_key=api_key, + api_url=api_url, + model_name=model_name, + temperature=temperature, + is_coding_endpoint=False, # Всегда False, так как используем общий endpoint + ) + + @classmethod + def from_env(cls): + """ + Создать конфигурацию из переменных окружения + """ + return cls( + api_key=os.getenv('ZAI_API_KEY', ''), + api_url=os.getenv('ZAI_API_URL', 'https://api.z.ai/api/paas/v4'), + model_name=os.getenv('ZAI_MODEL_NAME', 'glm-4.7'), + temperature=float(os.getenv('ZAI_TEMPERATURE', '0.7')), + is_coding_endpoint=os.getenv('ZAI_CODING_ENDPOINT', 'false').lower() == 'true', + ) + + +def get_glm_config(integration_model=None): + """ + Получить конфигурацию GLM из модели интеграции или из .env + """ + if integration_model: + return AIConfig.from_integration_model(integration_model) + else: + return AIConfig.from_env() + + +@dataclass +class OpenRouterConfig: + """ + Конфигурация для интеграции с OpenRouter + """ + api_key: str + api_url: str + model_name: str + temperature: float + max_tokens: int = 1000 + timeout: int = 30 + + @classmethod + def from_integration_model(cls, integration_model): + """ + Создать конфигурацию из модели интеграции + """ + logger.info(f"=== Создание конфигурации OpenRouter из модели интеграции ===") + logger.info(f"Тип модели: {type(integration_model)}") + + try: + api_key = integration_model.api_key + logger.info(f"API key (первые 10 символов): {api_key[:10] if api_key else 'None'}...") + except AttributeError as e: + logger.error(f"Ошибка при получении api_key: {str(e)}") + raise + + api_url = getattr(integration_model, 'api_url', 'https://openrouter.ai/api/v1') + logger.info(f"API URL: {api_url}") + + try: + model_name = integration_model.model_name + logger.info(f"Model name: {model_name}") + except AttributeError as e: + logger.error(f"Ошибка при получении model_name: {str(e)}") + raise + + try: + temperature = float(integration_model.temperature) + logger.info(f"Temperature: {temperature}") + except AttributeError as e: + logger.error(f"Ошибка при получении temperature: {str(e)}") + raise + + max_tokens = getattr(integration_model, 'max_tokens', 1000) + logger.info(f"Max tokens: {max_tokens}") + + return cls( + api_key=api_key, + api_url=api_url, + model_name=model_name, + temperature=temperature, + max_tokens=max_tokens, + ) + + @classmethod + def from_env(cls): + """ + Создать конфигурацию из переменных окружения + """ + return cls( + api_key=os.getenv('OPENROUTER_API_KEY', ''), + api_url=os.getenv('OPENROUTER_API_URL', 'https://openrouter.ai/api/v1'), + model_name=os.getenv('OPENROUTER_MODEL_NAME', 'xiaomi/mimo-v2-flash:free'), + temperature=float(os.getenv('OPENROUTER_TEMPERATURE', '0.7')), + max_tokens=int(os.getenv('OPENROUTER_MAX_TOKENS', '1000')), + ) + + +def get_openrouter_config(integration_model=None): + """ + Получить конфигурацию OpenRouter из модели интеграции или из .env + """ + if integration_model: + return OpenRouterConfig.from_integration_model(integration_model) + else: + return OpenRouterConfig.from_env() \ No newline at end of file diff --git a/myproject/integrations/services/ai_services/openrouter_service.py b/myproject/integrations/services/ai_services/openrouter_service.py new file mode 100644 index 0000000..5d76a1d --- /dev/null +++ b/myproject/integrations/services/ai_services/openrouter_service.py @@ -0,0 +1,200 @@ +from typing import Dict, Any, Tuple, Optional +from ..base import BaseIntegrationService +from .config import get_openrouter_config +import logging +import sys +import locale + +# Патч для исправления проблемы с кодировкой в httpx на Windows +# Устанавливаем кодировку по умолчанию для Python +if sys.platform == 'win32': + try: + import httpx._models + original_normalize_header_value = httpx._models._normalize_header_value + + def patched_normalize_header_value(value, encoding): + """Патч для использования UTF-8 вместо ASCII для заголовков""" + # Если значение уже bytes, возвращаем его как есть + if isinstance(value, bytes): + return value + # Всегда используем UTF-8 вместо ASCII + encoding = encoding or 'utf-8' + if encoding.lower() == 'ascii': + encoding = 'utf-8' + return value.encode(encoding) + + httpx._models._normalize_header_value = patched_normalize_header_value + logging.getLogger(__name__).info("Applied patch for httpx header encoding on Windows") + except Exception as e: + logging.getLogger(__name__).warning(f"Failed to apply httpx patch: {e}") + +from openai import OpenAI + +logger = logging.getLogger(__name__) + + +class OpenRouterIntegrationService(BaseIntegrationService): + """ + Сервис интеграции с OpenRouter.ai + """ + + def __init__(self, config): + super().__init__(config) + logger.info(f"=== Инициализация OpenRouterIntegrationService ===") + + try: + self.cfg = get_openrouter_config(config) + logger.info(f"Конфигурация успешно получена") + logger.info(f"API URL: {self.cfg.api_url}") + logger.info(f"Model name: {self.cfg.model_name}") + logger.info(f"Temperature: {self.cfg.temperature}") + logger.info(f"API key (первые 10 символов): {self.cfg.api_key[:10] if self.cfg.api_key else 'None'}...") + except Exception as e: + logger.error(f"Ошибка при получении конфигурации: {str(e)}", exc_info=True) + raise + + # Импортируем клиент OpenAI (OpenRouter совместим с OpenAI API) + try: + self.client = OpenAI( + api_key=self.cfg.api_key, + base_url=self.cfg.api_url, + ) + logger.info(f"OpenAI клиент для OpenRouter успешно создан") + except ImportError: + logger.error("Библиотека openai не установлена") + raise ImportError( + "Необходимо установить библиотеку openai: pip install openai" + ) + except Exception as e: + logger.error(f"Ошибка при создании клиента: {str(e)}", exc_info=True) + raise + + def test_connection(self) -> Tuple[bool, str]: + """ + Проверить соединение с API OpenRouter + """ + logger.info(f"=== Начало тестирования соединения с OpenRouter ===") + + try: + logger.info(f"Отправка запроса к API OpenRouter...") + logger.info(f"Model: {self.cfg.model_name}, Temperature: {self.cfg.temperature}") + + messages = [{"role": "user", "content": "ping"}] + + response = self.client.chat.completions.create( + model=self.cfg.model_name, + messages=messages, + temperature=self.cfg.temperature, + max_tokens=10 + ) + + logger.info(f"Получен ответ от API OpenRouter") + + if response.choices and len(response.choices) > 0: + logger.info(f"Успешное подключение к OpenRouter") + return True, "Connection to OpenRouter successful" + else: + logger.error(f"Некорректный ответ от API OpenRouter: нет choices") + return False, "Invalid response from OpenRouter API" + + except Exception as e: + logger.error(f"Ошибка подключения к OpenRouter: {str(e)}", exc_info=True) + return False, f"Connection error: {str(e)}" + + def sync(self) -> Tuple[bool, str]: + """ + Основная операция синхронизации (для OpenRouter не требуется) + """ + return True, "Синхронизация OpenRouter не требуется" + + def generate_text(self, + prompt: str, + system_prompt: Optional[str] = None, + max_tokens: Optional[int] = None) -> Tuple[bool, str, Optional[Dict]]: + """ + Генерация текста с помощью OpenRouter + + Args: + prompt: Входной текст для генерации + system_prompt: Системный промпт (опционально) + max_tokens: Максимальное количество токенов в ответе + + Returns: + tuple: (success: bool, message: str, response_data: dict or None) + """ + if not self.is_available(): + return False, "Интеграция OpenRouter не активна", None + + try: + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + + response = self.client.chat.completions.create( + model=self.cfg.model_name, + messages=messages, + temperature=self.cfg.temperature, + max_tokens=max_tokens or self.cfg.max_tokens + ) + + generated_text = response.choices[0].message.content + usage_info = getattr(response, 'usage', {}) + + return True, "Текст успешно сгенерирован", { + 'generated_text': generated_text, + 'usage': usage_info, + 'model': self.cfg.model_name + } + + except Exception as e: + logger.error(f"Ошибка генерации текста с помощью OpenRouter: {str(e)}") + return False, f"Ошибка генерации: {str(e)}", None + + def generate_code(self, + prompt: str, + language: Optional[str] = None, + max_tokens: Optional[int] = None) -> Tuple[bool, str, Optional[Dict]]: + """ + Генерация кода с помощью OpenRouter + + Args: + prompt: Описание задачи для генерации кода + language: Язык программирования (опционально) + max_tokens: Максимальное количество токенов в ответе + + Returns: + tuple: (success: bool, message: str, response_data: dict or None) + """ + if not self.is_available(): + return False, "Интеграция OpenRouter не активна", None + + try: + system_prompt = "You are a helpful AI coding assistant. Generate clean, efficient, and well-documented code." + if language: + system_prompt += f" The code should be in {language}." + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt} + ] + + response = self.client.chat.completions.create( + model=self.cfg.model_name, + messages=messages, + temperature=self.cfg.temperature, + max_tokens=max_tokens or self.cfg.max_tokens + ) + + generated_code = response.choices[0].message.content + usage_info = getattr(response, 'usage', {}) + + return True, "Код успешно сгенерирован", { + 'generated_code': generated_code, + 'usage': usage_info, + 'model': self.cfg.model_name + } + + except Exception as e: + logger.error(f"Ошибка генерации кода с помощью OpenRouter: {str(e)}") + return False, f"Ошибка генерации кода: {str(e)}", None