import os import pymysql from dotenv import load_dotenv import requests import json import re import time import sys import random from datetime import datetime # Загрузка переменных окружения load_dotenv() # Конфигурация из переменных окружения DB_CONFIG = { 'host': os.getenv('MDB_HOST'), 'user': os.getenv('MDB_USER'), 'password': os.getenv('MDB_PW'), 'database': os.getenv('MDBASE'), 'charset': 'utf8mb4' } ROUTERAI_CONFIG = { 'api_key': os.getenv('RA_KEY'), 'model': os.getenv('RA_MODEL'), 'base_url': 'https://routerai.ru/api/v1', } # Настройки журналирования LOG_FILE = os.getenv('LOG_FILE', 'ai_namer.log') # Путь к лог-файлу LOG_PREFIX = "AI_namer" # Уникальный префикс для идентификации скрипта def log_message(message, max_retries=5, retry_delay=0.1): """ Записывает сообщение в лог-файл с обработкой блокировок и идентификатором скрипта """ if not LOG_FILE: return timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') # Добавляем идентификатор скрипта к сообщению log_line = f"[{timestamp}] [{LOG_PREFIX}] {message}\n" for attempt in range(max_retries): try: with open(LOG_FILE, 'a', encoding='utf-8') as log: log.write(log_line) return True except (IOError, OSError) as e: if "locked" in str(e).lower() and attempt < max_retries - 1: # Случайная задержка для уменьшения коллизий sleep_time = retry_delay * (1 + random.random() * 0.5) time.sleep(sleep_time) else: # Если не удалось записать после всех попыток print(f"Ошибка записи в лог: {e}") print(f"Сообщение для лога: {log_line.strip()}") return False def _generate_shortname_by_api(text): """Внутренняя функция для генерации краткого названия через RouterAI API""" if not ROUTERAI_CONFIG['api_key'] or not ROUTERAI_CONFIG['model']: log_message("RA_KEY или RA_MODEL не заданы в .env") print("Ошибка: RA_KEY или RA_MODEL не заданы в .env") return None url = f"{ROUTERAI_CONFIG['base_url']}/chat/completions" headers = { "Authorization": f"Bearer {ROUTERAI_CONFIG['api_key']}", "Content-Type": "application/json", } prompt = f""" Прочитай нижеприведенный анонс и сформируй краткое (до 80 знаков) название этого события для каталога. Место, дата и время проведения для каталога несущественны. Важно название и, в зависимости от того, что это за событие, выступающие на нем или проводящие его. Для данного каталога допустимо использование никнеймов вместо официальных имен и фамилий. Ответ должен содержать только одно название без каких-либо объяснений, комментариев и вариантов. АНОНС: {text} Название:""" data = { "model": ROUTERAI_CONFIG['model'], "messages": [{"role": "user", "content": prompt}], "max_tokens": 5000, "temperature": 0.3, } try: response = requests.post(url, headers=headers, json=data, timeout=180) # Обработка ошибки 429 (Too Many Requests) if response.status_code == 429: error_info = { "status_code": response.status_code, "headers": dict(response.headers), "body": response.text } # Пытаемся распарсить JSON ответ try: error_info["body"] = response.json() except Exception: pass log_message(f"Ошибка 429: Превышен лимит запросов. Ответ API: {json.dumps(error_info, ensure_ascii=False)}") print("Ошибка 429: Превышен лимит запросов. Подробности в логе.") # Не sys.exit — эта функция вызывается и из веб-API Flask raise RuntimeError("RouterAI 429: превышен лимит запросов") response.raise_for_status() result = response.json() # Извлекаем только текст ответа (у thinking-моделей ответ в content, # рассуждения — в reasoning_content / reasoning) message = result['choices'][0]['message'] shortname = (message.get('content') or '').strip() if not shortname: log_message(f"Пустой content в ответе RouterAI: {json.dumps(result, ensure_ascii=False)[:1000]}") return None # Удаляем возможные кавычки и лишние символы shortname = re.sub(r'^["\']|["\']$', '', shortname) # Если модель вернула многострочный ответ — берём первую непустую строку for line in shortname.splitlines(): line = line.strip() if line: shortname = line break # Обрезаем до 80 символов return shortname[:80] except requests.exceptions.HTTPError as e: if e.response is not None and e.response.status_code == 429: error_info = { "status_code": e.response.status_code, "headers": dict(e.response.headers), "body": e.response.text } # Пытаемся распарсить JSON ответ try: error_info["body"] = e.response.json() except Exception: pass log_message(f"Ошибка 429: Превышен лимит запросов. Ответ API: {json.dumps(error_info, ensure_ascii=False)}") print("Ошибка 429: Превышен лимит запросов. Подробности в логе.") raise RuntimeError("RouterAI 429: превышен лимит запросов") else: error_msg = f"HTTP ошибка при генерации названия: {e}" log_message(error_msg) print(error_msg) return None except RuntimeError: raise except Exception as e: error_msg = f"Ошибка при генерации названия: {e}" log_message(error_msg) print(error_msg) return None def generate_ai_shortname(text): """ Генерирует краткое название для описанного в тексте мероприятия. Args: text (str): Текст описания мероприятия Returns: str: Краткое название (до 80 знаков) или None в случае ошибки """ log_message(f"Начало генерации названия для текста длиной {len(text)} символов") if not text or not text.strip(): log_message("Получен пустой текст для генерации названия") return None shortname = _generate_shortname_by_api(text) if shortname: log_message(f"Успешно сгенерировано название: {shortname}") else: log_message("Не удалось сгенерировать название") return shortname def ai_shortname_all(): """ Обрабатывает все записи в базе данных с пустым полем shortname, генерируя для них краткие названия с помощью нейросети. """ log_message("Запуск обработки всех записей с пустым shortname") # Подключение к БД try: connection = pymysql.connect(**DB_CONFIG) log_message("Успешное подключение к базе данных") except Exception as e: error_msg = f"Ошибка подключения к базе данных: {e}" log_message(error_msg) print(error_msg) return try: with connection.cursor() as cursor: # Выбор записей с пустым shortname cursor.execute("SELECT id, text FROM posts WHERE shortname IS NULL OR shortname = ''") posts = cursor.fetchall() log_message(f"Найдено {len(posts)} записей для обработки") print(f"Найдено {len(posts)} записей для обработки") for i, (post_id, text) in enumerate(posts, 1): if not text: continue log_message(f"Обрабатывается запись {i}/{len(posts)} (ID: {post_id})") print(f"Обрабатывается запись {i}/{len(posts)} (ID: {post_id})") shortname = generate_ai_shortname(text) if shortname: # Обновление записи try: cursor.execute( "UPDATE posts SET shortname = %s WHERE id = %s", (shortname, post_id) ) connection.commit() log_message(f"Запись {post_id} успешно обновлена: {shortname}") print(f"Обновлено: {shortname}") except Exception as e: error_msg = f"Ошибка обновления записи {post_id}: {e}" log_message(error_msg) print(error_msg) else: log_message(f"Не удалось сгенерировать название для записи {post_id}") print("Не удалось сгенерировать название") # Небольшая пауза между запросами time.sleep(1) log_message("Обработка всех записей завершена") print("Обработка завершена") except RuntimeError as e: # 429 и прочие явные ошибки RouterAI log_message(f"Завершение работы: {e}") print(f"Завершение работы: {e}") except Exception as e: error_msg = f"Произошла ошибка при обработке записей: {e}" log_message(error_msg) print(error_msg) finally: connection.close() log_message("Соединение с базой данных закрыто") if __name__ == "__main__": ai_shortname_all()