From 401993526b15e85d0dc72b671e6b135e13b2eaed Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Thu, 15 Jan 2026 12:16:56 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20GLM=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=B9=20=D0=B4=D0=BE=20GLM-4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0003_update_glm_default_model.py | 29 +++ .../migrations/0004_revert_glm_model.py | 29 +++ .../0005_update_glm_model_to_glm4.py | 29 +++ .../0006_update_glm_model_to_glm4.py | 29 +++ .../integrations/models/ai_services/glm.py | 89 +++++++ .../services/ai_services/glm_service.py | 218 ++++++++++++++++++ myproject/test_glm_models.py | 41 ++++ 7 files changed, 464 insertions(+) create mode 100644 myproject/integrations/migrations/0003_update_glm_default_model.py create mode 100644 myproject/integrations/migrations/0004_revert_glm_model.py create mode 100644 myproject/integrations/migrations/0005_update_glm_model_to_glm4.py create mode 100644 myproject/integrations/migrations/0006_update_glm_model_to_glm4.py create mode 100644 myproject/integrations/models/ai_services/glm.py create mode 100644 myproject/integrations/services/ai_services/glm_service.py create mode 100644 myproject/test_glm_models.py diff --git a/myproject/integrations/migrations/0003_update_glm_default_model.py b/myproject/integrations/migrations/0003_update_glm_default_model.py new file mode 100644 index 0000000..2fbb9d6 --- /dev/null +++ b/myproject/integrations/migrations/0003_update_glm_default_model.py @@ -0,0 +1,29 @@ +# Generated migration for updating GLM default model + +from django.db import migrations + + +def update_glm_default_model(apps, schema_editor): + """Обновляем модель по умолчанию для существующих записей GLM""" + GLMIntegration = apps.get_model('integrations', 'GLMIntegration') + + # Обновляем все записи с glm-4.7 на glm-4-flash + GLMIntegration.objects.filter(model_name='glm-4.7').update(model_name='glm-4-flash') + + +def reverse_update_glm_default_model(apps, schema_editor): + """Откат миграции - возвращаем glm-4.7""" + GLMIntegration = apps.get_model('integrations', 'GLMIntegration') + + # Возвращаем glm-4.7 + GLMIntegration.objects.filter(model_name='glm-4-flash').update(model_name='glm-4.7') + + +class Migration(migrations.Migration): + dependencies = [ + ('integrations', '0002_glmintegration_and_more'), + ] + + operations = [ + migrations.RunPython(update_glm_default_model, reverse_update_glm_default_model), + ] diff --git a/myproject/integrations/migrations/0004_revert_glm_model.py b/myproject/integrations/migrations/0004_revert_glm_model.py new file mode 100644 index 0000000..0e34cfb --- /dev/null +++ b/myproject/integrations/migrations/0004_revert_glm_model.py @@ -0,0 +1,29 @@ +# Generated migration for reverting GLM model back to glm-4.7 + +from django.db import migrations + + +def revert_glm_model(apps, schema_editor): + """Откатываем модель обратно на glm-4.7""" + GLMIntegration = apps.get_model('integrations', 'GLMIntegration') + + # Обновляем все записи с glm-4-flash обратно на glm-4.7 + GLMIntegration.objects.filter(model_name='glm-4-flash').update(model_name='glm-4.7') + + +def reverse_revert_glm_model(apps, schema_editor): + """Применяем миграцию - возвращаем glm-4-flash""" + GLMIntegration = apps.get_model('integrations', 'GLMIntegration') + + # Возвращаем glm-4-flash + GLMIntegration.objects.filter(model_name='glm-4.7').update(model_name='glm-4-flash') + + +class Migration(migrations.Migration): + dependencies = [ + ('integrations', '0003_update_glm_default_model'), + ] + + operations = [ + migrations.RunPython(revert_glm_model, reverse_revert_glm_model), + ] diff --git a/myproject/integrations/migrations/0005_update_glm_model_to_glm4.py b/myproject/integrations/migrations/0005_update_glm_model_to_glm4.py new file mode 100644 index 0000000..d05f2a1 --- /dev/null +++ b/myproject/integrations/migrations/0005_update_glm_model_to_glm4.py @@ -0,0 +1,29 @@ +# Generated migration for updating GLM model to glm-4 + +from django.db import migrations + + +def update_glm_model_to_glm4(apps, schema_editor): + """Обновляем модель с glm-4-flash на glm-4""" + GLMIntegration = apps.get_model('integrations', 'GLMIntegration') + + # Обновляем все записи с glm-4-flash на glm-4 + GLMIntegration.objects.filter(model_name='glm-4-flash').update(model_name='glm-4') + + +def reverse_update_glm_model_to_glm4(apps, schema_editor): + """Откат миграции - возвращаем glm-4-flash""" + GLMIntegration = apps.get_model('integrations', 'GLMIntegration') + + # Возвращаем glm-4-flash + GLMIntegration.objects.filter(model_name='glm-4').update(model_name='glm-4-flash') + + +class Migration(migrations.Migration): + dependencies = [ + ('integrations', '0004_revert_glm_model'), + ] + + operations = [ + migrations.RunPython(update_glm_model_to_glm4, reverse_update_glm_model_to_glm4), + ] diff --git a/myproject/integrations/migrations/0006_update_glm_model_to_glm4.py b/myproject/integrations/migrations/0006_update_glm_model_to_glm4.py new file mode 100644 index 0000000..9489ee2 --- /dev/null +++ b/myproject/integrations/migrations/0006_update_glm_model_to_glm4.py @@ -0,0 +1,29 @@ +# Generated migration for updating GLM model from glm-4-flash to glm-4 + +from django.db import migrations + + +def update_glm_model_to_glm4(apps, schema_editor): + """Обновляем модель с glm-4-flash на glm-4""" + GLMIntegration = apps.get_model('integrations', 'GLMIntegration') + + # Обновляем все записи с glm-4-flash на glm-4 + GLMIntegration.objects.filter(model_name='glm-4-flash').update(model_name='glm-4') + + +def reverse_update_glm_model_to_glm4(apps, schema_editor): + """Откат миграции - возвращаем glm-4-flash""" + GLMIntegration = apps.get_model('integrations', 'GLMIntegration') + + # Возвращаем glm-4-flash + GLMIntegration.objects.filter(model_name='glm-4').update(model_name='glm-4-flash') + + +class Migration(migrations.Migration): + dependencies = [ + ('integrations', '0005_update_glm_model_to_glm4'), + ] + + operations = [ + migrations.RunPython(update_glm_model_to_glm4, reverse_update_glm_model_to_glm4), + ] diff --git a/myproject/integrations/models/ai_services/glm.py b/myproject/integrations/models/ai_services/glm.py new file mode 100644 index 0000000..7cf09c8 --- /dev/null +++ b/myproject/integrations/models/ai_services/glm.py @@ -0,0 +1,89 @@ +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 до 1""" + if value < 0 or value > 1: + raise ValidationError('Температура должна быть в диапазоне 0.0-1.0') + + +# Список доступных моделей GLM (включая бесплатные) +GLM_MODEL_CHOICES = [ + ('glm-4', 'GLM-4 (Платная, дешевле)'), + ('glm-4.7', 'GLM-4.7 (Платная)'), + ('charglm-3', 'ChargLM-3 (Платная)'), + ('glm-4.6v', 'GLM-4.6V (Бесплатная)'), + ('glm-4.5v', 'GLM-4.5V (Бесплатная)'), + ('glm-4.5-air', 'GLM-4.5-Air (Бесплатная)'), + ('glm-4.5-flash', 'GLM-4.5-Flash (Бесплатная)'), + ('glm-4.5-flashx', 'GLM-4.5-FlashX (Бесплатная)'), +] + + +class AIIntegration(BaseIntegration): + """ + Базовая модель для интеграций с ИИ-сервисами + """ + class Meta: + abstract = True + + +class GLMIntegration(AIIntegration): + """ + Интеграция с GLM от Z.AI + """ + integration_type = IntegrationType.AI_SERVICE + + api_key = EncryptedCharField( + max_length=500, + blank=True, + verbose_name="API ключ", + help_text="Ключ для доступа к API GLM от Z.AI (шифруется в БД)" + ) + + api_url = models.URLField( + max_length=500, + default="https://api.z.ai/api/paas/v4", + verbose_name="URL API", + help_text="URL для обращения к API GLM (обычно https://api.z.ai/api/paas/v4)" + ) + + model_name = models.CharField( + max_length=100, + default="glm-4", + choices=GLM_MODEL_CHOICES, + verbose_name="Название модели", + help_text="Название используемой модели GLM (например, glm-4.7, glm-4)" + ) + + temperature = models.FloatField( + default=0.7, + validators=[validate_temperature], + verbose_name="Температура", + help_text="Параметр температуры для генерации (0.0-1.0)" + ) + + is_coding_endpoint = models.BooleanField( + default=False, + verbose_name="Использовать эндпоинт для кодинга", + help_text="Отметьте, если используете специальный эндпоинт для задач программирования" + ) + + class Meta: + verbose_name = "Интеграция GLM" + verbose_name_plural = "Интеграции GLM" + + @property + def is_configured(self) -> bool: + return bool(self.api_key) + + def clean(self): + super().clean() + if self.temperature < 0 or self.temperature > 1: + raise ValidationError({'temperature': 'Температура должна быть в диапазоне 0.0-1.0'}) + + # Всегда используем общий эндпоинт + self.api_url = "https://api.z.ai/api/paas/v4" \ No newline at end of file diff --git a/myproject/integrations/services/ai_services/glm_service.py b/myproject/integrations/services/ai_services/glm_service.py new file mode 100644 index 0000000..8599d14 --- /dev/null +++ b/myproject/integrations/services/ai_services/glm_service.py @@ -0,0 +1,218 @@ +from typing import Dict, Any, Tuple, Optional +from ..base import BaseIntegrationService +from django.conf import settings +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 zai import ZaiClient +from .config import get_glm_config + + +logger = logging.getLogger(__name__) + + +class GLMIntegrationService(BaseIntegrationService): + """ + Сервис интеграции с GLM от Z.AI + """ + + def __init__(self, config): + super().__init__(config) + logger.info(f"=== Инициализация GLMIntegrationService ===") + logger.info(f"Тип config: {type(config)}") + + # Получаем конфигурацию из модели интеграции + try: + self.cfg = get_glm_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 + + # Создаем клиент ZaiClient + try: + logger.info(f"Попытка создать ZaiClient...") + logger.info(f"API key type: {type(self.cfg.api_key)}") + logger.info(f"API key length: {len(self.cfg.api_key) if self.cfg.api_key else 0}") + logger.info(f"API key (первые 10 символов): {self.cfg.api_key[:10] if self.cfg.api_key else 'None'}...") + logger.info(f"API key (последние 10 символов): {self.cfg.api_key[-10:] if self.cfg.api_key and len(self.cfg.api_key) > 10 else 'None'}...") + + self.client = ZaiClient(api_key=self.cfg.api_key) + logger.info(f"ZaiClient успешно создан") + except Exception as e: + logger.error(f"Ошибка при создании ZaiClient: {str(e)}", exc_info=True) + raise + + def test_connection(self) -> Tuple[bool, str]: + """ + Проверить соединение с API GLM + """ + import sys + import locale + + logger.info(f"=== Начало тестирования соединения с GLM ===") + logger.info(f"Python default encoding: {sys.getdefaultencoding()}") + logger.info(f"Python filesystem encoding: {sys.getfilesystemencoding()}") + logger.info(f"Locale preferred encoding: {locale.getpreferredencoding()}") + + try: + # Отправляем простой запрос для проверки подключения + logger.info(f"Отправка запроса к API GLM...") + logger.info(f"Model: {self.cfg.model_name}, Temperature: {self.cfg.temperature}") + + # Используем английский текст для теста, чтобы избежать проблем с кодировкой + messages = [{"role": "user", "content": "ping"}] + logger.info(f"Messages: {messages}") + + response = self.client.chat.completions.create( + model=self.cfg.model_name, + messages=messages, + temperature=self.cfg.temperature, + max_tokens=10 + ) + + logger.info(f"Получен ответ от API GLM") + logger.info(f"Тип ответа: {type(response)}") + logger.info(f"Атрибуты ответа: {dir(response)}") + + # Проверяем, что получили ответ + if hasattr(response, 'choices') and len(response.choices) > 0: + logger.info(f"Успешное подключение к GLM") + return True, "Connection to GLM successful" + else: + logger.error(f"Некорректный ответ от API GLM: нет choices") + return False, "Invalid response from GLM API" + + except Exception as e: + logger.error(f"Ошибка подключения к GLM: {str(e)}", exc_info=True) + return False, f"Connection error: {str(e)}" + + def sync(self) -> Tuple[bool, str]: + """ + Основная операция синхронизации (в случае GLM - может быть вызовом API для обработки данных) + """ + # Реализация будет зависеть от конкретных требований + return True, "Синхронизация GLM не требуется" + + def generate_text(self, + prompt: str, + system_prompt: Optional[str] = None, + max_tokens: int = 1000) -> Tuple[bool, str, Optional[Dict]]: + """ + Генерация текста с помощью GLM + + Args: + prompt: Входной текст для генерации + system_prompt: Системный промпт (опционально) + max_tokens: Максимальное количество токенов в ответе + + Returns: + tuple: (success: bool, message: str, response_data: dict or None) + """ + if not self.is_available(): + return False, "Интеграция GLM не активна", 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 + ) + + # Извлекаем сгенерированный текст + 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"Ошибка генерации текста с помощью GLM: {str(e)}") + return False, f"Ошибка генерации: {str(e)}", None + + def generate_code(self, + prompt: str, + language: Optional[str] = None, + max_tokens: int = 1000) -> Tuple[bool, str, Optional[Dict]]: + """ + Генерация кода с помощью GLM (использует специальный эндпоинт, если указан) + + Args: + prompt: Описание задачи для генерации кода + language: Язык программирования (опционально) + max_tokens: Максимальное количество токенов в ответе + + Returns: + tuple: (success: bool, message: str, response_data: dict or None) + """ + if not self.is_available(): + return False, "Интеграция GLM не активна", 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 + ) + + 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"Ошибка генерации кода с помощью GLM: {str(e)}") + return False, f"Ошибка генерации кода: {str(e)}", None \ No newline at end of file diff --git a/myproject/test_glm_models.py b/myproject/test_glm_models.py new file mode 100644 index 0000000..4a61421 --- /dev/null +++ b/myproject/test_glm_models.py @@ -0,0 +1,41 @@ +""" +Скрипт для тестирования доступных моделей GLM +""" +from zai import ZaiClient + +# Список моделей для тестирования +models_to_test = [ + "glm-4.7", + "glm-4-flash", + "glm-4", + "glm-3", + "glm-4-air", + "glm-4-flashx", +] + +# Ваш API ключ (замените на свой) +api_key = "YOUR_API_KEY_HERE" + +client = ZaiClient(api_key=api_key) + +for model in models_to_test: + try: + print(f"Тестирование модели: {model}") + response = client.chat.completions.create( + model=model, + messages=[{"role": "user", "content": "ping"}], + temperature=0.7, + max_tokens=10 + ) + print(f"✅ Модель {model} работает!") + print(f" Ответ: {response.choices[0].message.content if response.choices else 'Нет ответа'}") + break # Если модель работает, останавливаем тест + except Exception as e: + error_msg = str(e) + if "Unknown Model" in error_msg or "1211" in error_msg: + print(f"❌ Модель {model} не существует") + elif "429" in error_msg or "1113" in error_msg: + print(f"✅ Модель {model} существует, но нет баланса") + break + else: + print(f"❌ Модель {model} вызвала ошибку: {error_msg}")