Files
Zilant2025/db_update_shortname.py
T
gitadmin 9b8b5514b8
ci/woodpecker/push/woodpecker Pipeline was successful
[ZILANT] Переход на RouterAI
2026-08-02 16:42:51 +03:00

255 lines
11 KiB
Python

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=60)
# Обработка ошибки 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(1)
response.raise_for_status()
result = response.json()
# Извлекаем только текст ответа
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)
# Обрезаем до 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: Превышен лимит запросов. Подробности в логе.")
sys.exit(1)
else:
error_msg = f"HTTP ошибка при генерации названия: {e}"
log_message(error_msg)
print(error_msg)
return None
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 SystemExit:
# Перехватываем системный выход для корректного закрытия соединения
log_message("Завершение работы из-за ошибки 429")
print("Завершение работы из-за ошибки 429")
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()