265 lines
12 KiB
Python
265 lines
12 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=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()
|