Доработка гит

This commit is contained in:
Niken
2026-03-15 20:36:16 +03:00
parent 4b50941b86
commit c49b00ba15
9 changed files with 680 additions and 200 deletions
+475 -110
View File
@@ -2,21 +2,71 @@ import base64
import aiohttp
import logging
from aiogram import Dispatcher, Bot
from aiogram.types import Message
from aiogram.types import Message, InlineKeyboardButton, CallbackQuery
from aiogram.utils.keyboard import InlineKeyboardBuilder
from utils.antispam import saving, save_message, admin_required
from aiogram.filters import Command
from models.state import BotState
import hashlib
import json
from typing import Dict, List, Tuple, Optional
logger = logging.getLogger(__name__)
# Глобальные переменные на уровне модуля
current_model = "google/gemma-3-12b"
model_hash_map: Dict[str, str] = {} # Словарь для хранения соответствия хэшей и полных названий моделей
# Список моделей, которые точно поддерживают изображения (можно расширять)
IMAGE_SUPPORTED_MODELS = [
"llava", # Модели LLaVA
"vision", # Модели с поддержкой vision
"clip", # CLIP модели
"qwen2-vl", # Qwen с vision
"cogvlm", # CogVLM
"paligemma", # PaLI-Gemma
"gemma-2-2b-it", # Gemma 2 может поддерживать
"gemma-2-9b-it",
"gemini", # Gemini
"llava-hf", # LLaVA HuggingFace
"moondream", # Moondream
]
def get_model_hash(model_name: str) -> str:
"""Создает короткий хэш для названия модели"""
return hashlib.md5(model_name.encode()).hexdigest()[:16]
def truncate_model_name(model_name: str, max_length: int = 40) -> str:
"""Сокращает название модели для отображения"""
if len(model_name) <= max_length:
return model_name
return model_name[:max_length - 3] + "..."
def supports_images(model_name: str) -> bool:
"""Проверяет, поддерживает ли модель изображения"""
model_lower = model_name.lower()
# Если модель содержит ключевые слова, связанные с поддержкой изображений
for keyword in IMAGE_SUPPORTED_MODELS:
if keyword.lower() in model_lower:
return True
return False
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
chat_history = {}
MAX_HISTORY = 20 # храним последние 20 сообщений (user+assistant)
MODEL = "google/gemma-3-12b"
#MODEL = "google/gemma-3n-e4b"
URL = "http://192.168.31.197:1234/v1/chat/completions"
#URL = "http://192.168.31.95:1234/v1/chat/completions"
# MODEL = "google/gemma-3n-e4b"
URL_BASE = "10.180.139.124"
URL = f"http://{URL_BASE}:1234/v1/chat/completions"
# URL = "http://192.168.31.95:1234/v1/chat/completions"
MODELS_URL = f"http://{URL_BASE}:1234/v1/models" # URL для получения списка моделей
# Используем глобальные переменные
global current_model, model_hash_map
# # Системный промт для всех чатов
SYSTEM_PROMPT = {
@@ -60,10 +110,171 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
# ],
# }
@dp.message(Command("gpt"))
@dp.message(Command("models"))
@admin_required(0)
@saving
async def ask_gpt(message: Message):
chat_id = message.chat.id
async def list_models(message: Message):
"""Получить список доступных моделей и выбрать одну"""
global model_hash_map, current_model
try:
async with aiohttp.ClientSession() as session:
async with session.get(MODELS_URL) as resp:
if resp.status != 200:
error_text = await resp.text()
await message.reply(
f"❌ Ошибка при получении списка моделей: {resp.status} {error_text}"
)
return
data = await resp.json()
# Формат ответа может отличаться в зависимости от API
models = []
# Формат OpenAI-compatible API
if isinstance(data, dict) and "data" in data:
models = [model["id"] for model in data["data"]]
# Другой возможный формат
elif isinstance(data, list):
models = data
# Если не распознали формат
else:
await message.reply(f"❌ Неизвестный формат ответа от API: {data}")
return
if not models:
await message.reply("❌ Нет доступных моделей")
return
# Создаем клавиатуру для выбора модели
builder = InlineKeyboardBuilder()
for model in models:
# Создаем короткий хэш для callback_data
model_hash = get_model_hash(model)
model_hash_map[model_hash] = model # Сохраняем в глобальный словарь
# Сокращаем для отображения
display_name = truncate_model_name(model)
# Помечаем текущую модель и добавляем иконку для моделей с поддержкой изображений
prefix = "" if model == current_model else ""
icon = "🖼️ " if supports_images(model) else ""
builder.row(
InlineKeyboardButton(
text=f"{prefix}{icon}{display_name}",
callback_data=f"model:{model_hash}"
)
)
# Кнопки для управления
builder.row(
InlineKeyboardButton(
text="🔄 Обновить список",
callback_data="refresh_models"
),
InlineKeyboardButton(
text="📋 Показать текущую",
callback_data="show_current"
),
InlineKeyboardButton(
text="🖼️ Модели с картинками",
callback_data="show_image_models"
)
)
# Сохраняем карту хэшей в файл для отладки (опционально)
try:
with open("model_hash_map.json", "w") as f:
json.dump(model_hash_map, f, indent=2, ensure_ascii=False)
except:
pass
# Разделяем модели на поддерживающие изображения и обычные
image_models = [m for m in models if supports_images(m)]
text_only_models = [m for m in models if not supports_images(m)]
models_list = "\n".join(
[f"{i + 1}. {model} {'🖼️' if supports_images(model) else ''}" for i, model in
enumerate(models[:10])])
if len(models) > 10:
models_list += f"\n... и еще {len(models) - 10} моделей"
await message.reply(
f"📋 Доступные модели ({len(models)} шт.):\n"
f"🖼️ Поддерживают изображения: {len(image_models)}\n"
f"📝 Только текст: {len(text_only_models)}\n"
f"Текущая модель: <code>{current_model}</code> {'🖼️' if supports_images(current_model) else '📝'}\n\n"
"Первые 10 моделей:\n" + models_list + "\n\n"
"Выберите модель из списка ниже (🖼️ - поддерживает изображения):",
reply_markup=builder.as_markup(),
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Ошибка при получении списка моделей: {e}")
await message.reply(f"❌ Ошибка при получении списка моделей: {e}")
@dp.message(Command("setmodel"))
@admin_required(0)
@saving
async def set_model(message: Message):
"""Установить модель через команду"""
global current_model, model_hash_map
if not message.text:
await message.reply("❌ Укажите название модели: /setmodel <название_модели>")
return
args = message.text.split(maxsplit=1)
if len(args) < 2:
await message.reply("❌ Укажите название модели: /setmodel <название_модели>")
return
new_model = args[1]
# Проверяем, есть ли модель в текущем кэше (опционально)
if model_hash_map:
model_hashes = list(model_hash_map.values())
if new_model not in model_hashes:
await message.reply(
f"⚠️ Модель <code>{new_model}</code> не найдена в последнем списке.\n"
f"Используйте /models для просмотра доступных моделей.\n"
f"Продолжаем смену модели...",
parse_mode="HTML"
)
old_model = current_model
current_model = new_model
# Сообщаем о поддержке изображений
image_support = "🖼️ поддерживает изображения" if supports_images(new_model) else "📝 только текст"
await message.reply(
f"✅ Модель успешно изменена! ({image_support})\n"
f"📊 Старая модель: <code>{old_model}</code>\n"
f"📈 Новая модель: <code>{current_model}</code>",
parse_mode="HTML"
)
@dp.message(Command("currentmodel"))
@saving
async def show_current_model(message: Message):
"""Показать текущую модель"""
global current_model
image_support = "🖼️ Поддерживает изображения" if supports_images(current_model) else "📝 Только текст"
await message.reply(
f"🤖 Текущая модель: <code>{current_model}</code>\n"
f"{image_support}",
parse_mode="HTML"
)
async def prepare_gpt_request(chat_id: int, message: Message) -> Tuple[Optional[List[dict]], Optional[str]]:
"""Подготавливает запрос к GPT, обрабатывая текст и изображения"""
if chat_id not in chat_history:
chat_history[chat_id] = [SYSTEM_PROMPT]
@@ -78,26 +289,42 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if message.caption:
user_prompt = message.caption
# Добавляем текстовый промпт, если есть
if user_prompt:
content_blocks.append({"type": "text", "text": user_prompt})
elif not message.photo:
# Если нет ни текста, ни фото
return None, "❗ Укажи текст или прикрепи фото"
# Фото → base64 → image_url
if message.photo:
photo = message.photo[-1]
file = await bot.get_file(photo.file_id)
file_bytes = await bot.download_file(file.file_path)
image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
content_blocks.append(
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
}
)
# Обрабатываем фото, если модель поддерживает изображения
if message.photo and supports_images(current_model):
try:
photo = message.photo[-1]
file = await bot.get_file(photo.file_id)
file_bytes = await bot.download_file(file.file_path)
image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
content_blocks.append(
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
}
)
except Exception as e:
logger.error(f"Ошибка при обработке изображения: {e}")
# Если не удалось обработать изображение, продолжаем без него
if not user_prompt:
return None, "❌ Ошибка при обработке изображения"
elif message.photo and not supports_images(current_model):
# Если модель не поддерживает изображения, отправляем текстовое описание
if not user_prompt:
# Если нет текста, просим переформулировать
return None, f"⚠️ Текущая модель <code>{current_model}</code> не поддерживает изображения.\nОтправьте текстовое описание или смените модель на поддерживающую изображения (🖼️)."
# Если есть текст, просто игнорируем изображение и отправляем текст
logger.info(f"Модель {current_model} не поддерживает изображения, отправляем только текст")
# Если после всех проверок content_blocks пустой
if not content_blocks:
await message.reply("❗ Укажи текст или прикрепи фото")
return
return None, "❗ Укажи текст или прикрепи фото"
# Добавляем новое сообщение в историю
chat_history[chat_id].append({"role": "user", "content": content_blocks})
@@ -106,27 +333,86 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
return chat_history[chat_id], None
@dp.message(Command("gpt"))
@saving
async def ask_gpt(message: Message):
global current_model
chat_id = message.chat.id
# Подготавливаем запрос
prepared_history, error_message = await prepare_gpt_request(chat_id, message)
if error_message:
await message.reply(error_message, parse_mode="HTML")
return
payload = {
"model": MODEL,
"messages": chat_history[chat_id],
"model": current_model,
"messages": prepared_history,
"temperature": 0.7,
"max_tokens": 4096,
"stream": False,
"ttl": 300,
}
# Флаг, указывающий на попытку отправки изображения
has_image = bool(message.photo)
image_attempt_failed = False
try:
async with aiohttp.ClientSession() as session:
async with session.post(URL, json=payload) as resp:
if resp.status != 200:
error_text = await resp.text()
await message.reply(
f"❌ Ошибка LM Studio: {resp.status} {error_text}"
)
return
data = await resp.json()
reply_text = data["choices"][0]["message"]["content"]
# Проверяем, если ошибка из-за изображения
if has_image and "does not support images" in error_text:
image_attempt_failed = True
logger.warning(
f"Модель {current_model} не поддерживает изображения, пробуем без изображения")
# Удаляем последнее сообщение из истории (с изображением)
chat_history[chat_id].pop()
# Пробуем отправить только текст
if message.caption:
# Используем caption как промпт
content_blocks = [{"type": "text", "text": message.caption}]
elif message.text:
parts = message.text.split(maxsplit=1)
if len(parts) > 1:
content_blocks = [{"type": "text", "text": parts[1]}]
else:
content_blocks = [{"type": "text", "text": "Опиши изображение"}]
else:
content_blocks = [{"type": "text", "text": "Опиши изображение"}]
# Добавляем текстовый запрос
chat_history[chat_id].append({"role": "user", "content": content_blocks})
# Обновляем payload
payload["messages"] = chat_history[chat_id]
# Повторяем запрос без изображения
async with session.post(URL, json=payload) as retry_resp:
if retry_resp.status != 200:
error_text = await retry_resp.text()
await message.reply(
f"❌ Ошибка LM Studio: {retry_resp.status} {error_text}"
)
return
data = await retry_resp.json()
reply_text = data["choices"][0]["message"]["content"]
else:
await message.reply(
f"❌ Ошибка LM Studio: {resp.status} {error_text}"
)
return
else:
data = await resp.json()
reply_text = data["choices"][0]["message"]["content"]
# Сохраняем ответ ассистента в историю
chat_history[chat_id].append(
@@ -140,14 +426,21 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
# Добавляем заметку о попытке отправки изображения
image_note = ""
if image_attempt_failed:
image_note = f"\n\n⚠️ Модель не поддерживает изображения, отправлен только текстовый запрос."
# Делим сообщение на части по 4000 символов
MAX_LEN = 4000
for i in range(0, len(reply_text), MAX_LEN):
chunk = reply_text[i:i + MAX_LEN]
msg = await message.reply(f"🤖 Ответ:\n{chunk}")
if i == 0:
msg = await message.reply(f"🤖 Ответ (модель: {current_model}){image_note}:\n{chunk}")
else:
msg = await message.reply(chunk)
save_message(msg.chat.id, msg.message_id)
except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}")
@@ -155,59 +448,27 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("agpt"))
@admin_required(0)
@saving
async def ask_gpt(message: Message):
async def ask_gpt_admin(message: Message):
global current_model
chat_id = message.chat.id
if chat_id not in chat_history:
chat_history[chat_id] = [SYSTEM_PROMPT]
content_blocks = []
user_prompt = None
# Текст после команды или caption
if message.text:
parts = message.text.split(maxsplit=1)
if len(parts) > 1:
user_prompt = parts[1]
if message.caption:
user_prompt = message.caption
if user_prompt:
content_blocks.append({"type": "text", "text": user_prompt})
# Фото → base64 → image_url
if message.photo:
photo = message.photo[-1]
file = await bot.get_file(photo.file_id)
file_bytes = await bot.download_file(file.file_path)
image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
content_blocks.append(
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
}
)
if not content_blocks:
await message.reply("❗ Укажи текст или прикрепи фото")
# Подготавливаем запрос
prepared_history, error_message = await prepare_gpt_request(chat_id, message)
if error_message:
await message.reply(error_message, parse_mode="HTML")
return
# Добавляем новое сообщение в историю
chat_history[chat_id].append({"role": "user", "content": content_blocks})
# Ограничиваем историю (оставляем последние MAX_HISTORY сообщений)
if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
payload = {
"model": MODEL,
"messages": chat_history[chat_id],
"model": current_model,
"messages": prepared_history,
"temperature": 0.7,
"max_tokens": 4096,
"stream": False,
"ttl": 300,
}
target_chat_id = -1003038389942
try:
async with aiohttp.ClientSession() as session:
async with session.post(URL, json=payload) as resp:
@@ -237,10 +498,10 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
MAX_LEN = 4000
for i in range(0, len(reply_text), MAX_LEN):
chunk = reply_text[i:i + MAX_LEN]
msg = await bot.send_message(chat_id=-1003038389942, text=f"{chunk}")
msg = await bot.send_message(chat_id=target_chat_id,
text=f"🤖 Ответ (модель: {current_model}):\n{chunk}")
save_message(msg.chat.id, msg.message_id)
except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}")
@@ -248,13 +509,12 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
@dp.message(Command("igpt"))
@admin_required(0)
@saving
async def ask_gpt(message: Message):
async def ask_gpt_interactive(message: Message):
global current_model
raw_text = message.text or message.caption
if not raw_text and not (
message.photo or message.document or message.audio or message.video
):
if not raw_text and not (message.photo):
await message.reply(
"❌ Укажи ID чата и текст или прикрепи файл/медиа: /igpt <chat_id> <сообщение>"
"❌ Укажи ID чата и текст или прикрепи фото: /igpt <chat_id> <сообщение>"
)
return
@@ -264,47 +524,60 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
return
try:
chat_id = int(args[1]) # первый аргумент — ID чата
target_chat_id = int(args[1]) # первый аргумент — ID чата
except ValueError:
await message.reply("❌ Неверный формат chat_id")
return
user_prompt = args[2] if len(args) > 2 else ""
if chat_id not in chat_history:
chat_history[chat_id] = [SYSTEM_PROMPT]
# Используем локальный chat_id для обработки запроса
local_chat_id = message.chat.id
if local_chat_id not in chat_history:
chat_history[local_chat_id] = [SYSTEM_PROMPT]
content_blocks = []
if user_prompt:
content_blocks.append({"type": "text", "text": user_prompt})
# Фото → base64 → image_url
if message.photo:
photo = message.photo[-1]
file = await bot.get_file(photo.file_id)
file_bytes = await bot.download_file(file.file_path)
image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
content_blocks.append(
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
}
)
# Фото → base64 → image_url (только если модель поддерживает)
if message.photo and supports_images(current_model):
try:
photo = message.photo[-1]
file = await bot.get_file(photo.file_id)
file_bytes = await bot.download_file(file.file_path)
image_b64 = base64.b64encode(file_bytes.read()).decode("utf-8")
content_blocks.append(
{
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{image_b64}"},
}
)
except Exception as e:
logger.error(f"Ошибка при обработке изображения: {e}")
if not user_prompt:
await message.reply("❌ Ошибка при обработке изображения")
return
elif message.photo and not supports_images(current_model):
await message.reply(f"⚠️ Текущая модель <code>{current_model}</code> не поддерживает изображения.",
parse_mode="HTML")
if not user_prompt:
return
if not content_blocks:
await message.reply("❗ Укажи текст или прикрепи фото")
return
# Добавляем новое сообщение в историю
chat_history[chat_id].append({"role": "user", "content": content_blocks})
chat_history[local_chat_id].append({"role": "user", "content": content_blocks})
# Ограничиваем историю (оставляем последние MAX_HISTORY сообщений)
if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
# Ограничиваем историю
if len(chat_history[local_chat_id]) > MAX_HISTORY:
chat_history[local_chat_id] = chat_history[local_chat_id][-MAX_HISTORY:]
payload = {
"model": MODEL,
"messages": chat_history[chat_id],
"model": current_model,
"messages": chat_history[local_chat_id],
"temperature": 0.7,
"max_tokens": 4096,
"stream": False,
@@ -325,29 +598,121 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
reply_text = data["choices"][0]["message"]["content"]
# Сохраняем ответ ассистента в историю
chat_history[chat_id].append(
chat_history[local_chat_id].append(
{
"role": "assistant",
"content": [{"type": "text", "text": reply_text}],
}
)
# Ограничиваем снова (чтобы не разрасталось)
if len(chat_history[chat_id]) > MAX_HISTORY:
chat_history[chat_id] = chat_history[chat_id][-MAX_HISTORY:]
# Ограничиваем снова
if len(chat_history[local_chat_id]) > MAX_HISTORY:
chat_history[local_chat_id] = chat_history[local_chat_id][-MAX_HISTORY:]
# Делим сообщение на части по 4000 символов
MAX_LEN = 4000
for i in range(0, len(reply_text), MAX_LEN):
chunk = reply_text[i:i + MAX_LEN]
msg = await bot.send_message(chat_id=chat_id, text=chunk)
msg = await bot.send_message(chat_id=target_chat_id,
text=f"🤖 Ответ (модель: {current_model}):\n{chunk}")
save_message(msg.chat.id, msg.message_id)
except Exception as e:
logger.error(f"Ошибка при запросе к LM Studio: {e}")
await message.reply(f"❌ Ошибка при запросе к LM Studio: {e}")
@dp.message(Command("clear"))
async def clear(message: Message):
chat_history.pop(message.chat.id, None)
chat_history.pop(message.chat.id, None)
await message.reply("✅ История диалога очищена")
# Создаем простые фильтры для callback
async def model_callback_filter(callback_query: CallbackQuery) -> bool:
return callback_query.data.startswith("model:")
async def refresh_models_filter(callback_query: CallbackQuery) -> bool:
return callback_query.data == "refresh_models"
async def show_current_filter(callback_query: CallbackQuery) -> bool:
return callback_query.data == "show_current"
async def show_image_models_filter(callback_query: CallbackQuery) -> bool:
return callback_query.data == "show_image_models"
@dp.callback_query(model_callback_filter)
async def select_model_callback(callback_query: CallbackQuery):
"""Обработка выбора модели из инлайн-клавиатуры"""
global current_model, model_hash_map
model_hash = callback_query.data.split(":", 1)[1]
# Пытаемся загрузить из файла, если в памяти нет
if not model_hash_map:
try:
with open("model_hash_map.json", "r") as f:
model_hash_map = json.load(f)
except Exception as e:
logger.error(f"Ошибка загрузки model_hash_map из файла: {e}")
# Получаем полное название модели из карты
if model_hash not in model_hash_map:
await callback_query.answer("❌ Ошибка: модель не найдена в кэше. Используйте /models для обновления списка",
show_alert=True)
return
model_id = model_hash_map[model_hash]
old_model = current_model
current_model = model_id
# Сообщаем о поддержке изображений
image_support = "🖼️ поддерживает изображения" if supports_images(model_id) else "📝 только текст"
# Обновляем сообщение
try:
await callback_query.message.edit_text(
f"✅ Модель успешно изменена! ({image_support})\n\n"
f"📊 <b>Старая модель:</b>\n<code>{old_model}</code>\n\n"
f"📈 <b>Новая модель:</b>\n<code>{current_model}</code>\n\n"
f"Для просмотра всех моделей используйте /models",
parse_mode="HTML"
)
except Exception as e:
logger.error(f"Ошибка при редактировании сообщения: {e}")
# Отправляем подтверждение
await callback_query.answer(f"✅ Модель изменена на:\n{truncate_model_name(model_id, 30)}")
@dp.callback_query(refresh_models_filter)
async def refresh_models_callback(callback_query: CallbackQuery):
"""Обновить список моделей"""
await list_models(callback_query.message)
await callback_query.answer("🔄 Список моделей обновлен")
@dp.callback_query(show_current_filter)
async def show_current_callback(callback_query: CallbackQuery):
"""Показать текущую модель"""
global current_model
image_support = "🖼️ Поддерживает изображения" if supports_images(current_model) else "📝 Только текст"
await callback_query.answer(f"Текущая модель: {current_model}\n{image_support}", show_alert=True)
@dp.callback_query(show_image_models_filter)
async def show_image_models_callback(callback_query: CallbackQuery):
"""Показать модели с поддержкой изображений"""
global model_hash_map
if not model_hash_map:
await callback_query.answer("❌ Список моделей пуст. Используйте /models", show_alert=True)
return
image_models = [model for model in model_hash_map.values() if supports_images(model)]
if not image_models:
await callback_query.answer("🖼️ Нет моделей с поддержкой изображений", show_alert=True)
return
models_text = "\n".join([f"{model}" for model in image_models[:10]])
if len(image_models) > 10:
models_text += f"\n... и еще {len(image_models) - 10} моделей"
await callback_query.answer(f"🖼️ Модели с поддержкой изображений ({len(image_models)} шт.):\n{models_text}",
show_alert=True)