Rid3 commited on
Commit
604263b
·
verified ·
1 Parent(s): 1913403

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +918 -179
app.py CHANGED
@@ -6,232 +6,971 @@ import io
6
  import traceback
7
  import requests
8
  import torch
9
- from fastapi import FastAPI, HTTPException
10
- from fastapi.middleware.cors import CORSMiddleware
11
- from pydantic import BaseModel
12
- import uvicorn
13
- from pyrogram import Client, filters, enums
14
- from gtts import gTTS
15
- from transformers import AutoModelForCausalLM, AutoTokenizer
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  # ============================================================
18
  # КОНФИГУРАЦИЯ БОТА
19
  # ============================================================
20
- API_ID = 39072129
21
- API_HASH = "5ed7516a5721300911826a9093397922"
22
- SESSION_STRING = "AgJUMYEANjf0BgCQGcJsNjo5Rs1siB3_DseD8Du7YbJOL9jk9QSWth2Lw2C5GolmYqM-KxQiXnXF_KF1P3BXTAIlATMoPokL-CMdQ4OrOcZdPtM3P8KPigL0JDljK95d2h-E6xSzmo9-TQcaZoYPpeYoNrcJ_Ii5R3AcjbQerf_GzadJFmuM-31sLHF9OP16tUtnnyngsY_V6hdlkhWZlDdnJV1TKRgwP-kViMRf84IYaiEEW7kGKNgqe8fU2OUir1-Xz-X-Cil_CgPDP0h7fOHYB0H74Ul_yq8XZI71C4MbcPwsEpp5LeGc8WBBXkIXD6r0AINmMiKDjb1EOwlIczlQ6gXsWQAAAAIA83VRAQ"
23
 
24
- # Используем полностью открытую и умную модель (не требует HF_TOKEN)
25
- MODEL_ID = "Qwen/Qwen2.5-1.5B-Instruct"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
  # ============================================================
28
- # ИНИЦИАЛИЗАЦИЯ ИИ (Локально через Transformers)
29
  # ============================================================
30
- print(f"🧠 Загрузка мозгов ИИ из {MODEL_ID} то займет немного времени при первом запуске)...")
 
 
31
 
32
- try:
33
- # Загружаем токенизатор и саму модель
34
- tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
35
- # Используем torch.float32 для CPU или torch.bfloat16 если есть мощный процессор
36
- model = AutoModelForCausalLM.from_pretrained(
37
- MODEL_ID,
38
- torch_dtype=torch.float32,
39
- device_map="cpu", # Принудительно на CPU, как вы и хотели
40
- low_cpu_mem_usage=True
41
- )
42
- print("🚀 INAI готов к работе (Локальная модель загружена)!")
43
- except Exception as e:
44
- print(f"❌ Ошибка загрузки модели: {e}")
45
- traceback.print_exc()
 
46
 
 
 
 
 
47
 
48
  # ============================================================
49
- # FASTAPI НАСТРОЙКИ
50
  # ============================================================
51
- app = FastAPI(title="INAI Unified API")
52
- app.add_middleware(
53
- CORSMiddleware,
54
- allow_origins=["*"],
55
- allow_credentials=True,
56
- allow_methods=["*"],
57
- allow_headers=["*"],
58
- )
59
 
60
- class ChatRequest(BaseModel):
61
- prompt: str
62
- system_prompt: str = "You are INAI, created by RID3 (@Rid3_inc)."
63
- max_tokens: int = 512
64
- temperature: float = 0.7
 
 
 
65
 
66
  # ============================================================
67
- # PYROGRAM БОТ
68
  # ============================================================
69
- bot = Client(
70
- "inai_local_bot",
71
- session_string=SESSION_STRING,
72
- api_id=API_ID,
73
- api_hash=API_HASH,
74
- in_memory=True
75
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  # ============================================================
78
- # ЯДРО ГЕНЕРАЦИИ ТЕКСТА
79
  # ============================================================
80
- async def get_ai_response(prompt: str, user_name: str = "User", system_prompt: str = None, is_photo: bool = False, max_tokens: int = 512, temperature: float = 0.7):
81
- if not system_prompt:
82
- system_prompt = (
83
- f"You are INAI, an AI created by RID3 (@Rid3_inc). "
84
- f"The user's name is {user_name}. Always respond in Russian language. "
85
- "For image requests, end your message with [DRAW]: followed by an English description."
86
- )
87
-
88
- # Формируем структуру сообщений
89
- messages = [
90
- {"role": "system", "content": system_prompt},
91
- {"role": "user", "content": prompt}
92
- ]
93
-
94
- # Применяем шаблон чата модели
95
- prompt_text = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
96
-
97
- def generate():
98
- inputs = tokenizer(prompt_text, return_tensors="pt").to(model.device)
99
-
100
- # Генерация ответа
101
- outputs = model.generate(
102
- **inputs,
103
- max_new_tokens=max_tokens,
104
- temperature=temperature,
105
- do_sample=True,
106
- pad_token_id=tokenizer.eos_token_id
107
- )
108
-
109
- # Декодируем только новый сгенерированный текст
110
- generated_ids = outputs[0][inputs['input_ids'].shape[-1]:]
111
- return tokenizer.decode(generated_ids, skip_special_tokens=True)
112
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  try:
114
- # Запускаем тяжелую генерацию в отдельном потоке, чтобы не блокировать бота и API
115
- text = await asyncio.to_thread(generate)
116
- text = text.strip()
117
-
118
- # Обработка тега рисования
119
- img_p = None
120
- if "[DRAW]:" in text or is_photo:
121
- m = re.search(r'\[DRAW\]:\s*([^\n]+)', text, re.I)
122
- img_p = m.group(1).strip() if m else f"{prompt}, high quality digital art"
123
- if m:
124
- text = text.replace(m.group(0), "").strip()
125
-
126
- return text, img_p
 
 
 
 
 
127
  except Exception as e:
128
- return f" Ошибка генерации: {str(e)}", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  # ============================================================
131
- # ФУНКЦИИ МУЛЬТИМЕДИА (Бесплатно, без API ключей)
132
  # ============================================================
133
- async def generate_voice(text: str):
134
- def sync_tts():
135
- clean = re.sub(r'[*_`#]', '', text) # Убираем markdown
136
- tts = gTTS(text=clean, lang='ru', slow=False)
137
- fp = io.BytesIO()
138
- tts.write_to_fp(fp)
139
- fp.name = 'voice.ogg'
140
- fp.seek(0)
141
- return fp
142
- return await asyncio.to_thread(sync_tts)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
  # ============================================================
145
- # РОУТЫ FASTAPI
146
  # ============================================================
147
- @app.get("/")
148
- async def health():
149
- return {"status": "online", "message": f"API & Telegram Bot are running on {MODEL_ID}. Use POST /chat"}
150
 
151
- @app.post("/chat")
152
- async def chat(request: ChatRequest):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  try:
154
- text, _ = await get_ai_response(
155
- prompt=request.prompt,
156
- system_prompt=request.system_prompt,
157
- max_tokens=request.max_tokens,
158
- temperature=request.temperature
 
 
 
159
  )
160
- return {"response": text, "model": MODEL_ID}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  except Exception as e:
162
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
163
 
164
  # ============================================================
165
- # ОБРАБОТЧИКИ ТЕЛЕГРАМ БОТА
166
  # ============================================================
167
- @bot.on_message((filters.text | filters.caption) & ~filters.me)
168
- async def main_handler(client, message):
 
 
 
 
 
 
 
 
 
 
 
169
  try:
170
- text = message.text or message.caption or ""
171
- is_group = message.chat.type in [enums.ChatType.GROUP, enums.ChatType.SUPERGROUP]
172
-
173
- user_query = text
174
- should_respond = not is_group
175
-
176
- if is_group:
177
- triggers = ["inai", "инэй", "бот"]
178
- if any(text.lower().startswith(t) for t in triggers):
179
- should_respond = True
180
- user_query = re.sub(r"^(inai|инэй|бот)\s*", "", text, flags=re.IGNORECASE)
181
- elif message.reply_to_message and message.reply_to_message.from_user.is_self:
182
- should_respond = True
183
-
184
- if not should_respond or not user_query.strip():
185
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
- is_photo = any(kw in user_query.lower() for kw in ["нарисуй", "фото", "рисуй", "картинка"])
188
- is_audio = any(kw in user_query.lower() for kw in ["скажи", "озвучь", "голос"])
 
 
 
 
 
 
 
189
 
190
- status = await message.reply_text("💠 INAI думает (может занять время на CPU)...")
 
 
 
 
 
191
 
192
- ai_text, img_prompt = await get_ai_response(
193
- prompt=user_query,
194
- user_name=message.from_user.first_name,
195
- is_photo=is_photo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  )
 
197
 
198
- if is_audio and ai_text:
199
- await status.edit_text("🎤 Генерирую голос...")
200
- voice = await generate_voice(ai_text)
201
- if voice:
202
- await message.reply_voice(voice, caption="🔊 INAI Voice")
203
- await status.delete()
204
- return
205
-
206
- if img_prompt:
207
- await status.edit_text("🎨 Рисую...")
208
- img_url = f"https://image.pollinations.ai/prompt/{urllib.parse.quote(img_prompt)}?nologo=true"
209
- res = await asyncio.to_thread(lambda: requests.get(img_url, timeout=40))
210
- if res.status_code == 200:
211
- await message.reply_photo(io.BytesIO(res.content), caption=f"✨ {ai_text}"[:1024])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  await status.delete()
213
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- await status.edit_text(ai_text or "⚠️ Пустой ответ.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
- except Exception:
218
- print(traceback.format_exc())
219
 
220
  # ============================================================
221
- # АСИНХРОННЫЙ ЗАПУСК
222
  # ============================================================
223
- async def main():
224
- print("🚀 Запуск Telegram бота...")
225
- await bot.start()
226
-
227
- print("🌐 Запуск FastAPI сервера на порту 7860...")
228
- config = uvicorn.Config(app=app, host="0.0.0.0", port=7860, log_level="info")
229
- server = uvicorn.Server(config)
230
-
231
- await server.serve()
232
-
233
- print("🛑 Остановка бота...")
234
- await bot.stop()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
  if __name__ == "__main__":
237
- asyncio.run(main())
 
6
  import traceback
7
  import requests
8
  import torch
9
+ import logging
10
+ import random
11
+ import json
12
+ from telegram import (
13
+ Update,
14
+ InlineKeyboardButton,
15
+ InlineKeyboardMarkup,
16
+ InputFile,
17
+ )
18
+ from telegram.ext import (
19
+ Application,
20
+ CommandHandler,
21
+ MessageHandler,
22
+ CallbackQueryHandler,
23
+ ContextTypes,
24
+ filters,
25
+ )
26
+ from telegram.constants import ParseMode, ChatAction
27
+
28
+ # ============================================================
29
+ # НАСТРОЙКА ЛОГИРОВАНИЯ
30
+ # ============================================================
31
+ logging.basicConfig(
32
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
33
+ level=logging.INFO,
34
+ )
35
+ logger = logging.getLogger(__name__)
36
 
37
  # ============================================================
38
  # КОНФИГУРАЦИЯ БОТА
39
  # ============================================================
40
+ BOT_TOKEN = "8605889873:AAE2gV2t0psXKlj-8h9ksyyoCrWa8Q-TdkA"
 
 
41
 
42
+ # ============================================================
43
+ # СПИСОК КЛЮЧЕЙ GEMINI (при ошибке одного — используется следующий)
44
+ # ============================================================
45
+ GEMINI_API_KEYS = [
46
+ # Добавьте сюда ваши ключи Gemini API, например:
47
+ # "AIzaSy...",
48
+ # "AIzaSy...",
49
+ # "AIzaSy...",
50
+ # Пример-заглушка (замените на настоящие ключи):
51
+ "ЗАМЕНИТЕ_НА_ВАШИ_КЛЮЧИ_GEMINI_API",
52
+ ]
53
+
54
+ # Текущий индекс ключа (глобальный, потокобезопасный через asyncio.Lock)
55
+ current_key_index = 0
56
+ key_lock = asyncio.Lock()
57
+
58
+ GEMINI_MODEL = "gemini-2.5-flash-preview-04-17"
59
+ GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"
60
 
61
  # ============================================================
62
+ # СИСТЕМНЫЙ ПРОМПТ ДЛЯ INAI
63
  # ============================================================
64
+ INAI_SYSTEM_PROMPT = """Ты INAI #RID3, искусственный интеллект, созданный RID3 СЕРВИСАМИ 🤖✨
65
+ Твоё полное имя: INAI #RID3
66
+ Создатель: RID3 СЕРВИСАМИ (@Rid3_inc)
67
 
68
+ ПРАВИЛА ПОВЕДЕНИЯ:
69
+ 🔹 Ты активно используешь эмодзи в каждом ответе — это твой фирменный стиль
70
+ 🔹 Ты очень хорошо разбираешься в программировании — это твоя ГЛАВНАЯ специализация
71
+ 🔹 Ты помогаешь с кодом на Python, JavaScript, TypeScript, Rust, Go, C++, SQL и других языках
72
+ 🔹 Ты объясняешь концепции программирования понятно и с примерами
73
+ 🔹 Ты можешь создавать изображения по запросу пользователя (но это второстепенная функция)
74
+ 🔹 Всегда отвечай на языке пользователя (русский или другой)
75
+ 🔹 Ты дружелюбный, умный и полезный ИИ-ассист��нт
76
+ 🔹 Если пользователь просит нарисовать/создать изображение — добавь в конце своего ответа специальный тег: [DRAW]: описание на английском языке
77
+
78
+ СТИЛЬ ОТВЕТОВ:
79
+ Всегда используй эмодзи
80
+ Для кода используй блоки кода с указанием языка
81
+ ✅ Давай развёрнутые и полезные ответы
82
+ ✅ Будь позитивным и поддерживающим
83
 
84
+ ЕСЛИ ХОЧЕШЬ ПРЕДЛОЖИТЬ ПОЛЬЗОВАТЕЛЮ ВАРИАНТЫ ДЕЙСТВИЙ — добавь в конце ответа JSON с кнопками в таком формате:
85
+ [BUTTONS]: [{"text": "Текст кнопки 1", "action": "действие1"}, {"text": "Текст кнопки 2", "action": "действие2"}]
86
+
87
+ НЕ предлагай кнопки при каждом ответе — только когда это реально уместно и полезно."""
88
 
89
  # ============================================================
90
+ # КЛЮЧЕВЫЕ СЛОВА ДЛЯ ТРИГГЕРА ИИ В ГРУППАХ
91
  # ============================================================
92
+ AI_TRIGGER_KEYWORDS = [
93
+ "inai", "инэй", "инаи", "ии", "ai", "бот", "bot",
94
+ "искусственный интеллект", "нейросеть", "нейро",
95
+ "помоги", "помогите", "скажи", "ответь", "объясни",
96
+ "что такое", "как сделать", "напиши код", "помоги с кодом",
97
+ ]
 
 
98
 
99
+ # Ключевые слова для генерации изображений
100
+ IMAGE_KEYWORDS = [
101
+ "нарисуй", "нарисовать", "рисуй", "нарисовал бы",
102
+ "создай картинку", "сгенерируй", "сгенерируй картинку",
103
+ "сгенерируй изображение", "создай изображение",
104
+ "draw", "generate image", "create image",
105
+ "фото", "картинка", "изображение",
106
+ ]
107
 
108
  # ============================================================
109
+ # ФУНКЦИИ РАБОТЫ С GEMINI API (С РОТАЦИЕЙ КЛЮЧЕЙ)
110
  # ============================================================
111
+
112
+ async def get_next_key():
113
+ """Возвращает следующий доступный ключ по кругу."""
114
+ global current_key_index
115
+ async with key_lock:
116
+ key = GEMINI_API_KEYS[current_key_index]
117
+ current_key_index = (current_key_index + 1) % len(GEMINI_API_KEYS)
118
+ return key, current_key_index - 1 if current_key_index > 0 else len(GEMINI_API_KEYS) - 1
119
+
120
+
121
+ async def call_gemini_api(prompt: str, system_prompt: str = None, history: list = None) -> str:
122
+ """
123
+ Отправляет запрос к Gemini API с автоматической ротацией ключей.
124
+ При ошибке одного ключа переходит к следующему.
125
+ """
126
+ if system_prompt is None:
127
+ system_prompt = INAI_SYSTEM_PROMPT
128
+
129
+ # Формируем сообщения (история + новый запрос)
130
+ messages = []
131
+ if history:
132
+ for item in history:
133
+ messages.append(item)
134
+ messages.append({"role": "user", "parts": [{"text": prompt}]})
135
+
136
+ # Формируем тело запроса
137
+ request_body = {
138
+ "system_instruction": {
139
+ "parts": [{"text": system_prompt}]
140
+ },
141
+ "contents": messages,
142
+ "generationConfig": {
143
+ "temperature": 0.8,
144
+ "topK": 40,
145
+ "topP": 0.95,
146
+ "maxOutputTokens": 2048,
147
+ },
148
+ "safetySettings": [
149
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
150
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
151
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
152
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
153
+ ],
154
+ }
155
+
156
+ # Пробуем каждый ключ по очереди
157
+ tried_keys = set()
158
+ attempts = 0
159
+ max_attempts = len(GEMINI_API_KEYS)
160
+
161
+ while attempts < max_attempts:
162
+ key, key_idx = await get_next_key()
163
+
164
+ if key_idx in tried_keys:
165
+ # Если уже пробовали этот ключ — пропускаем
166
+ attempts += 1
167
+ continue
168
+
169
+ tried_keys.add(key_idx)
170
+ attempts += 1
171
+
172
+ url = GEMINI_API_URL.format(model=GEMINI_MODEL, key=key)
173
+
174
+ try:
175
+ logger.info(f"🔑 Используем ключ #{key_idx + 1} для запроса к Gemini API")
176
+
177
+ response = await asyncio.to_thread(
178
+ lambda: requests.post(
179
+ url,
180
+ json=request_body,
181
+ headers={"Content-Type": "application/json"},
182
+ timeout=60,
183
+ )
184
+ )
185
+
186
+ if response.status_code == 200:
187
+ data = response.json()
188
+ # Извлекаем текст ответа
189
+ try:
190
+ text = data["candidates"][0]["content"]["parts"][0]["text"]
191
+ logger.info(f"✅ Успешный ответ от Gemini (ключ #{key_idx + 1})")
192
+ return text
193
+ except (KeyError, IndexError) as e:
194
+ logger.error(f"❌ Ошибка парсинга ответа Gemini: {e} | Ответ: {data}")
195
+ continue
196
+
197
+ elif response.status_code == 429:
198
+ logger.warning(f"⚠️ Ключ #{key_idx + 1} превысил лимит (429). Переключаем ключ...")
199
+ continue
200
+
201
+ elif response.status_code == 401 or response.status_code == 403:
202
+ logger.warning(f"⚠️ Ключ #{key_idx + 1} недействителен ({response.status_code}). Переключаем ключ...")
203
+ continue
204
+
205
+ elif response.status_code == 503:
206
+ logger.warning(f"⚠️ Сервис Gemini временно недоступен (503). Пробуем другой ключ...")
207
+ continue
208
+
209
+ else:
210
+ logger.error(f"❌ Ошибка Gemini API (ключ #{key_idx + 1}): {response.status_code} | {response.text[:300]}")
211
+ continue
212
+
213
+ except requests.exceptions.Timeout:
214
+ logger.error(f"⏰ Таймаут запроса (ключ #{key_idx + 1}). Переключаем ключ...")
215
+ continue
216
+ except requests.exceptions.ConnectionError as e:
217
+ logger.error(f"🔌 Ошибка соединения (ключ #{key_idx + 1}): {e}")
218
+ continue
219
+ except Exception as e:
220
+ logger.error(f"💥 Неожиданная ошибка (ключ #{key_idx + 1}): {e}")
221
+ traceback.print_exc()
222
+ continue
223
+
224
+ # Если все ключи не сработали
225
+ return "❌ К сожалению, все доступные ключи API временно недоступны. Попробуйте позже! 🙏"
226
+
227
 
228
  # ============================================================
229
+ # ПАРСИНГ ОТВЕТА ИИ (кнопки, рисунки и т.д.)
230
  # ============================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
+ def parse_ai_response(text: str):
233
+ """
234
+ Парсит ответ ИИ:
235
+ - Извлекает кнопки из [BUTTONS]: [...]
236
+ - Извлекает промпт для изображения из [DRAW]: описание
237
+ - Возвращает (чистый текст, список кнопок или None, промпт для картинки или None)
238
+ """
239
+ clean_text = text
240
+ buttons = None
241
+ draw_prompt = None
242
+
243
+ # Ищем кнопки
244
+ buttons_match = re.search(r'\[BUTTONS\]:\s*(\[.*?\])', text, re.DOTALL)
245
+ if buttons_match:
246
+ try:
247
+ buttons_json = buttons_match.group(1)
248
+ buttons = json.loads(buttons_json)
249
+ # Убираем тег из текста
250
+ clean_text = clean_text.replace(buttons_match.group(0), "").strip()
251
+ except json.JSONDecodeError as e:
252
+ logger.warning(f"⚠️ Не удалось распарсить кнопки: {e}")
253
+ buttons = None
254
+
255
+ # Ищем запрос на рисование
256
+ draw_match = re.search(r'\[DRAW\]:\s*([^\n\[]+)', clean_text, re.IGNORECASE)
257
+ if draw_match:
258
+ draw_prompt = draw_match.group(1).strip()
259
+ clean_text = clean_text.replace(draw_match.group(0), "").strip()
260
+
261
+ # Убираем лишние пустые строки
262
+ clean_text = re.sub(r'\n{3,}', '\n\n', clean_text).strip()
263
+
264
+ return clean_text, buttons, draw_prompt
265
+
266
+
267
+ def build_inline_keyboard(buttons: list) -> InlineKeyboardMarkup | None:
268
+ """
269
+ Создаёт InlineKeyboardMarkup из списка кнопок.
270
+ Формат кнопок: [{"text": "...", "action": "..."}]
271
+ """
272
+ if not buttons:
273
+ return None
274
+
275
+ keyboard_rows = []
276
+ row = []
277
+ for i, btn in enumerate(buttons):
278
+ button_text = btn.get("text", "Кнопка")
279
+ button_action = btn.get("action", "action")
280
+ # Callback data ограничена 64 байтами в Telegram
281
+ callback_data = f"ai_action:{button_action[:50]}"
282
+ row.append(InlineKeyboardButton(button_text, callback_data=callback_data))
283
+
284
+ # По 2 кнопки в ряд
285
+ if len(row) == 2 or i == len(buttons) - 1:
286
+ keyboard_rows.append(row)
287
+ row = []
288
+
289
+ if keyboard_rows:
290
+ return InlineKeyboardMarkup(keyboard_rows)
291
+ return None
292
+
293
+
294
+ # ============================================================
295
+ # ГЕНЕРАЦИЯ ИЗОБРАЖЕНИЙ (Pollinations.ai — бесплатно)
296
+ # ============================================================
297
+
298
+ async def generate_image(prompt: str) -> bytes | None:
299
+ """Генерирует изображение через Pollinations.ai (бесплатно, без API ключа)."""
300
  try:
301
+ encoded_prompt = urllib.parse.quote(prompt)
302
+ # Добавляем случайный seed для разнообразия
303
+ seed = random.randint(1, 999999)
304
+ img_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?nologo=true&seed={seed}&width=1024&height=1024"
305
+
306
+ logger.info(f"🎨 Генерирую изображение: {prompt[:100]}...")
307
+
308
+ response = await asyncio.to_thread(
309
+ lambda: requests.get(img_url, timeout=60)
310
+ )
311
+
312
+ if response.status_code == 200 and response.content:
313
+ logger.info(f"✅ Изображение успешно сгенерировано ({len(response.content)} байт)")
314
+ return response.content
315
+ else:
316
+ logger.error(f"❌ Ошибка генерации изображения: {response.status_code}")
317
+ return None
318
+
319
  except Exception as e:
320
+ logger.error(f"💥 Ошибка при генерации изображения: {e}")
321
+ return None
322
+
323
+
324
+ # ============================================================
325
+ # ХРАНИЛИЩЕ ИСТОРИИ ДИАЛОГОВ (в памяти, для каждого чата)
326
+ # ============================================================
327
+ conversation_history: dict[int, list] = {}
328
+
329
+ MAX_HISTORY_LENGTH = 20 # Максимум сообщений в истории
330
+
331
+
332
+ def get_history(chat_id: int) -> list:
333
+ """Возвращает историю диалога для чата."""
334
+ return conversation_history.get(chat_id, [])
335
+
336
+
337
+ def add_to_history(chat_id: int, role: str, text: str):
338
+ """Добавляет сообщение в историю диалога."""
339
+ if chat_id not in conversation_history:
340
+ conversation_history[chat_id] = []
341
+
342
+ conversation_history[chat_id].append({
343
+ "role": role,
344
+ "parts": [{"text": text}]
345
+ })
346
+
347
+ # Обрезаем историю если слишком длинная
348
+ if len(conversation_history[chat_id]) > MAX_HISTORY_LENGTH:
349
+ conversation_history[chat_id] = conversation_history[chat_id][-MAX_HISTORY_LENGTH:]
350
+
351
+
352
+ def clear_history(chat_id: int):
353
+ """Очищает историю диалога для чата."""
354
+ if chat_id in conversation_history:
355
+ del conversation_history[chat_id]
356
+
357
+
358
+ # ============================================================
359
+ # ПРОВЕРКА: ДОЛЖЕН ЛИ ИИ ОТВЕЧАТЬ НА ЭТО СООБЩЕНИЕ?
360
+ # ============================================================
361
+
362
+ def should_ai_respond(message_text: str, is_group: bool, is_reply_to_bot: bool) -> tuple[bool, str]:
363
+ """
364
+ Проверяет, должен ли ИИ ответить на сообщение.
365
+ Возвращает (должен_отвечать, очищенный_запрос)
366
+ """
367
+ if not message_text:
368
+ return False, ""
369
+
370
+ text_lower = message_text.lower().strip()
371
+
372
+ # В личных сообщениях ИИ отвечает всегда
373
+ if not is_group:
374
+ return True, message_text
375
+
376
+ # В группах — только если это ответ на сообщение бота
377
+ if is_reply_to_bot:
378
+ return True, message_text
379
+
380
+ # В группах — проверяем наличие триггерных слов
381
+ for keyword in AI_TRIGGER_KEYWORDS:
382
+ if text_lower.startswith(keyword):
383
+ # Убираем триггерное слово из начала
384
+ cleaned = re.sub(
385
+ r'^' + re.escape(keyword) + r'[,\s]*',
386
+ '',
387
+ message_text,
388
+ flags=re.IGNORECASE
389
+ ).strip()
390
+ return True, cleaned if cleaned else message_text
391
+
392
+ if keyword in text_lower and len(keyword) > 3:
393
+ # Для более длинных ключевых слов — проверяем вхождение
394
+ return True, message_text
395
+
396
+ return False, ""
397
+
398
 
399
  # ============================================================
400
+ # ОБРАБОТЧИКИ КОМАНД
401
  # ============================================================
402
+
403
+ async def start_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
404
+ """Обработчик команды /start."""
405
+ user = update.effective_user
406
+ first_name = user.first_name if user else "Пользователь"
407
+
408
+ welcome_text = (
409
+ f"👋 Привет, {first_name}! Я — **INAI #RID3** 🤖✨\n\n"
410
+ f"🧠 Я искусственный интеллект, созданный **RID3 СЕРВИСАМИ**\n"
411
+ f"💻 Моя главная специализация — **программирование и технологии**!\n\n"
412
+ f"**Что я умею:**\n"
413
+ f"🔹 Помогать с кодом (Python, JS, Rust, Go, C++ и многое другое)\n"
414
+ f"🔹 Объяснять концепции программирования\n"
415
+ f"🔹 Дебажить и оптимизировать код\n"
416
+ f"🔹 Создавать изображения по запросу 🎨\n"
417
+ f"🔹 Отвечать на любые вопросы\n\n"
418
+ f"**В группах** — напишите **INAI**, **AI**, **БОТ** или ответьте на моё сообщение!\n\n"
419
+ f"💡 Просто напишите мне что-нибудь, и я помогу! 🚀"
420
+ )
421
+
422
+ keyboard = InlineKeyboardMarkup([
423
+ [
424
+ InlineKeyboardButton("💻 Пример кода", callback_data="example:code"),
425
+ InlineKeyboardButton("🎨 Нарисовать", callback_data="example:draw"),
426
+ ],
427
+ [
428
+ InlineKeyboardButton("❓ Как использовать", callback_data="example:help"),
429
+ InlineKeyboardButton("🗑️ Очистить историю", callback_data="action:clear_history"),
430
+ ],
431
+ ])
432
+
433
+ await update.message.reply_text(
434
+ welcome_text,
435
+ parse_mode=ParseMode.MARKDOWN,
436
+ reply_markup=keyboard,
437
+ )
438
+
439
+
440
+ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
441
+ """Обработчик команды /help."""
442
+ help_text = (
443
+ "📚 **Справка по INAI #RID3**\n\n"
444
+ "**Команды:**\n"
445
+ "🔸 `/start` — Начать работу с ботом\n"
446
+ "🔸 `/help` — Показать эту справку\n"
447
+ "🔸 `/clear` — Очистить историю диалога\n"
448
+ "🔸 `/draw [описание]` — Сгенерировать изображе��ие\n\n"
449
+ "**Как общаться в группах:**\n"
450
+ "✅ Начните сообщение с: `INAI`, `AI`, `ИИ`, `БОТ`, `BOT`\n"
451
+ "✅ Ответьте на моё сообщение\n\n"
452
+ "**Примеры запросов:**\n"
453
+ "💬 `INAI, напиши функцию сортировки на Python`\n"
454
+ "💬 `AI помоги с этим кодом`\n"
455
+ "💬 `нарисуй красивый закат на море`\n\n"
456
+ "🔥 **Специализация:** Программирование и технологии\n"
457
+ "🎨 **Дополнительно:** Генерация изображений\n\n"
458
+ "Создан RID3 СЕРВИСАМИ (@Rid3_inc) 🚀"
459
+ )
460
+
461
+ await update.message.reply_text(help_text, parse_mode=ParseMode.MARKDOWN)
462
+
463
+
464
+ async def clear_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
465
+ """Обработчик команды /clear — очищает историю диалога."""
466
+ chat_id = update.effective_chat.id
467
+ clear_history(chat_id)
468
+
469
+ await update.message.reply_text(
470
+ "🗑️ История диалога очищена! Начинаем с чистого листа ✨\n"
471
+ "Чем могу помочь? 💻"
472
+ )
473
+
474
+
475
+ async def draw_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
476
+ """Обработчик команды /draw — генерирует изображение."""
477
+ if not context.args:
478
+ await update.message.reply_text(
479
+ "🎨 Укажите описание изображения!\n"
480
+ "Пример: `/draw красивый закат на море, цифровое искусство`",
481
+ parse_mode=ParseMode.MARKDOWN,
482
+ )
483
+ return
484
+
485
+ prompt = " ".join(context.args)
486
+ await handle_image_generation(update, context, prompt, original_text=prompt)
487
+
488
 
489
  # ============================================================
490
+ # ОСНОВНОЙ ОБРАБОТЧИК СООБЩЕНИЙ
491
  # ============================================================
 
 
 
492
 
493
+ async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE):
494
+ """Обрабатывает входящие текстовые сообщения."""
495
+ if not update.message:
496
+ return
497
+
498
+ message = update.message
499
+ chat_id = update.effective_chat.id
500
+ user = update.effective_user
501
+ user_name = user.first_name if user else "Пользователь"
502
+
503
+ # Получаем текст сообщения
504
+ message_text = message.text or message.caption or ""
505
+ if not message_text:
506
+ return
507
+
508
+ # Определяем тип чата
509
+ is_group = message.chat.type in ["group", "supergroup"]
510
+ is_channel = message.chat.type == "channel"
511
+
512
+ # В каналах не отвечаем
513
+ if is_channel:
514
+ return
515
+
516
+ # Проверяем — это ответ на сообщение бота?
517
+ is_reply_to_bot = (
518
+ message.reply_to_message is not None
519
+ and message.reply_to_message.from_user is not None
520
+ and message.reply_to_message.from_user.is_bot
521
+ and message.reply_to_message.from_user.username == context.bot.username
522
+ )
523
+
524
+ # Определяем — должен ли ИИ отвечать
525
+ should_respond, cleaned_query = should_ai_respond(message_text, is_group, is_reply_to_bot)
526
+
527
+ if not should_respond:
528
+ # ИИ не должен отвечать — отвечает программа
529
+ if is_group:
530
+ # В группах не спамим каждым сообщением
531
+ # Отвечаем только если упомянули бота но без запроса
532
+ text_lower = message_text.lower()
533
+ for keyword in AI_TRIGGER_KEYWORDS[:6]: # Только основные ключевые слова
534
+ if text_lower.startswith(keyword) and not cleaned_query:
535
+ await message.reply_text(
536
+ "💬 Привет! Я здесь 👋\n"
537
+ "Задайте мне вопрос или напишите что нужно сделать!\n"
538
+ "Например: `INAI, помоги с кодом на Python` 💻",
539
+ parse_mode=ParseMode.MARKDOWN,
540
+ )
541
+ return
542
+ return
543
+
544
+ # Проверяем — запрос на изображение?
545
+ query_lower = cleaned_query.lower()
546
+ wants_image = any(kw in query_lower for kw in IMAGE_KEYWORDS)
547
+
548
+ # Показываем индикатор "печатает..."
549
+ await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
550
+
551
+ # Добавляем сообщение пол��зователя в историю
552
+ add_to_history(chat_id, "user", cleaned_query)
553
+
554
+ # Отправляем статус
555
+ status_message = await message.reply_text("💠 INAI думает... 🧠")
556
+
557
  try:
558
+ # Получаем ответ от Gemini
559
+ history = get_history(chat_id)
560
+ # Убираем последнее сообщение из истории (оно будет передано как prompt)
561
+ history_without_last = history[:-1] if history else []
562
+
563
+ ai_response = await call_gemini_api(
564
+ prompt=cleaned_query,
565
+ history=history_without_last if history_without_last else None,
566
  )
567
+
568
+ # Парсим ответ ИИ
569
+ clean_text, buttons, draw_prompt = parse_ai_response(ai_response)
570
+
571
+ # Добавляем ответ ИИ в историю
572
+ add_to_history(chat_id, "model", clean_text)
573
+
574
+ # Проверяем — хочет ли ИИ нарисовать картинку
575
+ if draw_prompt or wants_image:
576
+ final_draw_prompt = draw_prompt if draw_prompt else cleaned_query
577
+ await handle_image_generation(
578
+ update, context,
579
+ prompt=final_draw_prompt,
580
+ original_text=clean_text,
581
+ buttons=buttons,
582
+ status_message=status_message,
583
+ )
584
+ return
585
+
586
+ # Формируем клавиатуру если ИИ предложил кнопки
587
+ keyboard = build_inline_keyboard(buttons) if buttons else None
588
+
589
+ # Отправляем текстовый ответ
590
+ # Разбиваем на части если слишком длинный (лимит Telegram — 4096 символов)
591
+ if len(clean_text) > 4000:
592
+ parts = split_text(clean_text, max_length=4000)
593
+ # Первая часть — редактируем статус
594
+ await status_message.edit_text(
595
+ parts[0],
596
+ parse_mode=ParseMode.MARKDOWN,
597
+ )
598
+ # Остальные части — отправляем новыми сообщениями
599
+ for part in parts[1:]:
600
+ await message.reply_text(part, parse_mode=ParseMode.MARKDOWN)
601
+ # Кнопки добавляем к последнему сообщению
602
+ if keyboard:
603
+ await message.reply_text(
604
+ "💡 Выберите дальнейшее действие:",
605
+ reply_markup=keyboard,
606
+ )
607
+ else:
608
+ await status_message.edit_text(
609
+ clean_text,
610
+ parse_mode=ParseMode.MARKDOWN,
611
+ reply_markup=keyboard,
612
+ )
613
+
614
  except Exception as e:
615
+ logger.error(f"💥 Ошибка обработки сообщения: {e}")
616
+ traceback.print_exc()
617
+ try:
618
+ await status_message.edit_text(
619
+ f"❌ Упс! Что-то пошло не так 😅\n"
620
+ f"Попробуйте ещё раз или напишите `/clear` для сброса диалога."
621
+ )
622
+ except Exception:
623
+ pass
624
+
625
 
626
  # ============================================================
627
+ # ОБРАБОТЧИК ГЕНЕРАЦИИ ИЗОБРАЖЕНИЙ
628
  # ============================================================
629
+
630
+ async def handle_image_generation(
631
+ update: Update,
632
+ context: ContextTypes.DEFAULT_TYPE,
633
+ prompt: str,
634
+ original_text: str = None,
635
+ buttons: list = None,
636
+ status_message=None,
637
+ ):
638
+ """Генерирует и отправляет изображение."""
639
+ message = update.message
640
+ chat_id = update.effective_chat.id
641
+
642
  try:
643
+ # Обновляем статус
644
+ if status_message:
645
+ await status_message.edit_text("🎨 Рисую... Подождите немного! ✨")
646
+ else:
647
+ status_message = await message.reply_text("🎨 Рисую... Подождите немного! ✨")
648
+
649
+ # Показываем действие загрузки фото
650
+ await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.UPLOAD_PHOTO)
651
+
652
+ # Генерируем изображение
653
+ image_bytes = await generate_image(prompt)
654
+
655
+ if image_bytes:
656
+ # Удаляем статусное сообщение
657
+ try:
658
+ await status_message.delete()
659
+ except Exception:
660
+ pass
661
+
662
+ # Формируем подпись
663
+ caption_text = ""
664
+ if original_text and original_text.strip():
665
+ caption_text = original_text[:900] # Лимит подписи — 1024 символа
666
+ caption_text += f"\n\n🎨 *Запрос:* _{prompt[:100]}_"
667
+ else:
668
+ caption_text = f"🎨 Вот твоё изображение!\n\n*Запрос:* _{prompt[:200]}_"
669
+
670
+ # Формируем клавиатуру
671
+ keyboard = build_inline_keyboard(buttons) if buttons else InlineKeyboardMarkup([
672
+ [
673
+ InlineKeyboardButton("🔄 Перегенерировать", callback_data=f"regen:{prompt[:50]}"),
674
+ InlineKeyboardButton("💬 Задать вопрос", callback_data="action:ask_question"),
675
+ ]
676
+ ])
677
+
678
+ # Отправляем изображение
679
+ await message.reply_photo(
680
+ photo=io.BytesIO(image_bytes),
681
+ caption=caption_text,
682
+ parse_mode=ParseMode.MARKDOWN,
683
+ reply_markup=keyboard,
684
+ )
685
 
686
+ else:
687
+ # Ошибка генерации
688
+ error_text = (
689
+ "😕 Не удалось сгенерировать изображение.\n"
690
+ "Попробуйте изменить описание или повторите запрос чуть позже!\n\n"
691
+ "💡 *Совет:* Попробуйте более подробное описание на английском языке."
692
+ )
693
+ if original_text:
694
+ error_text = original_text + "\n\n" + error_text
695
 
696
+ keyboard = build_inline_keyboard(buttons)
697
+ await status_message.edit_text(
698
+ error_text,
699
+ parse_mode=ParseMode.MARKDOWN,
700
+ reply_markup=keyboard,
701
+ )
702
 
703
+ except Exception as e:
704
+ logger.error(f"💥 Ошибка при отправке изображения: {e}")
705
+ traceback.print_exc()
706
+ try:
707
+ await status_message.edit_text(
708
+ "❌ Ошибка при генерации изображения 😅\n"
709
+ "Попробуйте ещё раз!"
710
+ )
711
+ except Exception:
712
+ pass
713
+
714
+
715
+ # ============================================================
716
+ # ОБРАБОТЧИК НАЖАТИЙ НА КНОПКИ
717
+ # ============================================================
718
+
719
+ async def handle_callback_query(update: Update, context: ContextTypes.DEFAULT_TYPE):
720
+ """Обрабатывает нажатия на inline-кнопки."""
721
+ query = update.callback_query
722
+ await query.answer() # Убираем "часики" на кнопке
723
+
724
+ data = query.data
725
+ chat_id = update.effective_chat.id
726
+ user = update.effective_user
727
+ user_name = user.first_name if user else "Пользователь"
728
+
729
+ logger.info(f"📲 Нажата кнопка: {data} (пользователь: {user_name})")
730
+
731
+ # ---- Действия с историей ----
732
+ if data == "action:clear_history":
733
+ clear_history(chat_id)
734
+ await query.edit_message_text(
735
+ "🗑️ История диалога очищена! Начинаем с чистого листа ✨\n"
736
+ "Чем могу помочь? 💻"
737
  )
738
+ return
739
 
740
+ # ---- Пример использования ----
741
+ if data == "example:code":
742
+ await query.edit_message_text(
743
+ "💻 **Примеры запросов для кода:**\n\n"
744
+ "🔸 `Напиши функцию быстрой сортировки на Python`\n"
745
+ "🔸 `Как исправить эту ошибку: TypeError...`\n"
746
+ "🔸 `Объясни разницу между async/await и threading`\n"
747
+ "🔸 `Создай REST API на FastAPI`\n"
748
+ "🔸 `Напиши SQL запрос для выборки топ-10 пользователей`\n\n"
749
+ "Просто напишите мне свой запрос! 🚀",
750
+ parse_mode=ParseMode.MARKDOWN,
751
+ )
752
+ return
753
+
754
+ if data == "example:draw":
755
+ await query.edit_message_text(
756
+ "🎨 **Примеры запросов для генерации изображений:**\n\n"
757
+ "🔸 `нарисуй красивый закат на море`\n"
758
+ "🔸 `создай картинку кибер-города будущего`\n"
759
+ "🔸 `нарисуй котёнка программиста за ноутбуком`\n"
760
+ "🔸 `/draw futuristic robot in neon city, digital art`\n\n"
761
+ "Или просто напишите мне что хотите увидеть! ✨",
762
+ parse_mode=ParseMode.MARKDOWN,
763
+ )
764
+ return
765
+
766
+ if data == "example:help":
767
+ await query.edit_message_text(
768
+ "❓ **Как использовать INAI #RID3:**\n\n"
769
+ "**В личных сообщениях:**\n"
770
+ "✅ Просто напишите любой вопрос или задачу\n\n"
771
+ "**В группах:**\n"
772
+ "✅ Начните с: `INAI`, `AI`, `ИИ`, `БОТ`\n"
773
+ "✅ Или ответьте на моё сообщение\n\n"
774
+ "**Специальные команды:**\n"
775
+ "🔸 `/start` — Начать\n"
776
+ "🔸 `/help` — Справка\n"
777
+ "🔸 `/clear` — Очистить историю\n"
778
+ "🔸 `/draw [описание]` — Нарисовать\n\n"
779
+ "**Я лучше всего умею:**\n"
780
+ "💻 Помогать с программированием\n"
781
+ "🔧 Дебажить код\n"
782
+ "📚 Объяснять концепции\n"
783
+ "🎨 Создавать изображения\n\n"
784
+ "Создан RID3 СЕРВИСАМИ (@Rid3_inc) 🚀",
785
+ parse_mode=ParseMode.MARKDOWN,
786
+ )
787
+ return
788
+
789
+ if data == "action:ask_question":
790
+ await query.message.reply_text(
791
+ "💬 Задайте мне вопрос! Я готов помочь 🤖✨\n\n"
792
+ "Могу помочь с программированием, объяснить что-то,\n"
793
+ "или создать ещё одно изображение по вашему описанию 🎨"
794
+ )
795
+ return
796
+
797
+ # ---- Перегенерация изображения ----
798
+ if data.startswith("regen:"):
799
+ original_prompt = data[6:]
800
+ await query.message.reply_text("🔄 Генерирую новый вариант... ✨")
801
+ # Создаём фиктивный update для передачи в handle_image_generation
802
+ await handle_image_generation(
803
+ update=update,
804
+ context=context,
805
+ prompt=original_prompt,
806
+ original_text=f"🔄 Новый вариант изображения по запросу: _{original_prompt}_",
807
+ )
808
+ return
809
+
810
+ # ---- Действия от ИИ (динамические кнопки) ----
811
+ if data.startswith("ai_action:"):
812
+ action = data[10:] # Убираем префикс "ai_action:"
813
+
814
+ # Показываем статус
815
+ await context.bot.send_chat_action(chat_id=chat_id, action=ChatAction.TYPING)
816
+ status = await query.message.reply_text("💠 INAI обрабатывает ваш выбор... 🧠")
817
+
818
+ # Отправляем действие как запрос к ИИ
819
+ action_prompt = f"Пользователь нажал кнопку с действием: '{action}'. Выполни это действие или ответь на выбор пользователя."
820
+ add_to_history(chat_id, "user", action_prompt)
821
+
822
+ history = get_history(chat_id)
823
+ history_without_last = history[:-1] if history else []
824
+
825
+ ai_response = await call_gemini_api(
826
+ prompt=action_prompt,
827
+ history=history_without_last if history_without_last else None,
828
+ )
829
+
830
+ clean_text, new_buttons, draw_prompt = parse_ai_response(ai_response)
831
+ add_to_history(chat_id, "model", clean_text)
832
+
833
+ # Если ИИ хочет нарисовать
834
+ if draw_prompt:
835
+ try:
836
  await status.delete()
837
+ except Exception:
838
+ pass
839
+ await handle_image_generation(
840
+ update=update,
841
+ context=context,
842
+ prompt=draw_prompt,
843
+ original_text=clean_text,
844
+ buttons=new_buttons,
845
+ )
846
+ return
847
+
848
+ # Обычный текстовый ответ
849
+ keyboard = build_inline_keyboard(new_buttons)
850
+ try:
851
+ await status.edit_text(
852
+ clean_text,
853
+ parse_mode=ParseMode.MARKDOWN,
854
+ reply_markup=keyboard,
855
+ )
856
+ except Exception:
857
+ # Если текст не поддерживает Markdown — отправляем без форматирования
858
+ await status.edit_text(clean_text, reply_markup=keyboard)
859
+ return
860
+
861
+ # Неизвестная кнопка
862
+ logger.warning(f"⚠️ Неизвестный callback: {data}")
863
+ await query.answer("⚠️ Неизвестное действие", show_alert=False)
864
+
865
+
866
+ # ============================================================
867
+ # ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ
868
+ # ============================================================
869
+
870
+ def split_text(text: str, max_length: int = 4000) -> list[str]:
871
+ """Разбивает длинный текст на части для отправки в Telegram."""
872
+ if len(text) <= max_length:
873
+ return [text]
874
+
875
+ parts = []
876
+ while text:
877
+ if len(text) <= max_length:
878
+ parts.append(text)
879
+ break
880
 
881
+ # Ищем подходящее место для разрыва (по абзацу или предложению)
882
+ split_pos = max_length
883
+
884
+ # Пробуем разбить по двойному переносу строки
885
+ pos = text.rfind('\n\n', 0, max_length)
886
+ if pos > max_length // 2:
887
+ split_pos = pos + 2
888
+ else:
889
+ # Пробуем по одиночному переносу строки
890
+ pos = text.rfind('\n', 0, max_length)
891
+ if pos > max_length // 2:
892
+ split_pos = pos + 1
893
+ else:
894
+ # Пробуем по точке
895
+ pos = text.rfind('. ', 0, max_length)
896
+ if pos > max_length // 2:
897
+ split_pos = pos + 2
898
+
899
+ parts.append(text[:split_pos])
900
+ text = text[split_pos:]
901
+
902
+ return parts
903
+
904
+
905
+ # ============================================================
906
+ # ОБРАБОТЧИК ОШИБОК
907
+ # ============================================================
908
+
909
+ async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE):
910
+ """Глобальный обработчик ошибок."""
911
+ logger.error(f"💥 Ошибка при обработке обновления: {context.error}")
912
+ traceback.print_exc()
913
 
 
 
914
 
915
  # ============================================================
916
+ # ЗАПУСК БОТА
917
  # ============================================================
918
+
919
+ def main():
920
+ """Точка входа — запуск Telegram бота."""
921
+ print("=" * 60)
922
+ print("🤖 INAI #RID3 Telegram Bot")
923
+ print("🏢 Создан RID3 СЕРВИСАМИ")
924
+ print("🧠 Модель: Gemini 2.5 Flash")
925
+ print("=" * 60)
926
+
927
+ # Проверяем наличие ключей
928
+ valid_keys = [k for k in GEMINI_API_KEYS if k and not k.startswith("ЗАМЕНИТЕ")]
929
+ if not valid_keys:
930
+ print("❌ ОШИБКА: Не указаны ключи Gemini API!")
931
+ print(" Добавьте ваши ключи в список GEMINI_API_KEYS")
932
+ print(" Получить ключи: https://aistudio.google.com/app/apikey")
933
+ return
934
+
935
+ print(f"✅ Загружено {len(valid_keys)} ключей Gemini API")
936
+ print(f"🚀 Запускаю бота...")
937
+
938
+ # Создаём приложение
939
+ application = (
940
+ Application.builder()
941
+ .token(BOT_TOKEN)
942
+ .build()
943
+ )
944
+
945
+ # Регистрируем обработчики команд
946
+ application.add_handler(CommandHandler("start", start_command))
947
+ application.add_handler(CommandHandler("help", help_command))
948
+ application.add_handler(CommandHandler("clear", clear_command))
949
+ application.add_handler(CommandHandler("draw", draw_command))
950
+
951
+ # Регистрируем обработчик нажатий на кнопки
952
+ application.add_handler(CallbackQueryHandler(handle_callback_query))
953
+
954
+ # Регистрируем обработчик текстовых сообщений
955
+ application.add_handler(
956
+ MessageHandler(
957
+ filters.TEXT & ~filters.COMMAND,
958
+ handle_message,
959
+ )
960
+ )
961
+
962
+ # Регистрируем глобальный обработчик ошибок
963
+ application.add_error_handler(error_handler)
964
+
965
+ print("✅ Бот успешно запущен! Нажмите Ctrl+C для остановки.")
966
+ print("=" * 60)
967
+
968
+ # Запускаем polling
969
+ application.run_polling(
970
+ allowed_updates=Update.ALL_TYPES,
971
+ drop_pending_updates=True,
972
+ )
973
+
974
 
975
  if __name__ == "__main__":
976
+ main()