refactor: review fixes and code improvements
- Add shared proxy module (bot/utils/proxy.py) to eliminate duplicate SOCKS5 parsing - Fix AI client: escape HTML before Markdown→HTML conversion, unify timeouts, make health check optional and disabled by default, handle 429 retries - Fix ai.py: correct forwarded message handling (aiogram 3.x forward_origin), pass relevant summaries via extra_system_content - Fix dialogue.py: only respond to Astra's messages, use system context instead of prompt injection, answer on the limit message before phase transition - Fix voice.py: load Whisper model in thread pool, safe WAV path generation - Improve database.py: composite indexes, Boolean is_active, upsert file_id cache, add context cleanup helper - Update weather.py and yadisk_download.py to use shared proxy connector - Update yadisk.py: validate URL before cache clear, add download size limit, wrap sync file ops in to_thread - Reuse S3 client via lru_cache - Update setup_commands with /aiclear and /aiuser - Update README, Dockerfile (Python 3.11), docker-compose (mount models) - Pin dependency versions, remove unused httpx[socks] - Add basic pytest tests for layout converter and voice normalization
This commit is contained in:
+135
-47
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aiogram import Router, F
|
||||
from aiogram.types import Message
|
||||
@@ -15,11 +16,19 @@ from bot.utils.database import (
|
||||
clear_dialogue,
|
||||
clear_user_context,
|
||||
get_or_create_dialogue,
|
||||
increment_dialogue_count,
|
||||
save_chat_user,
|
||||
)
|
||||
from bot.utils.memory import save_summary_with_embedding, find_relevant_summaries, generate_summary
|
||||
from config import AI_BLOCK_DEFAULT_DURATION, AI_CONTEXT_LIMIT, AI_DIALOGUE_LIMIT, AI_PHASE2_LIMIT
|
||||
from bot.utils.memory import (
|
||||
save_summary_with_embedding,
|
||||
find_relevant_summaries,
|
||||
generate_summary,
|
||||
)
|
||||
from config import (
|
||||
AI_BLOCK_DEFAULT_DURATION,
|
||||
AI_CONTEXT_LIMIT,
|
||||
AI_DIALOGUE_LIMIT,
|
||||
AI_PHASE2_LIMIT,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -48,8 +57,8 @@ async def get_user_id_by_username(message: Message, username: str) -> int | None
|
||||
|
||||
if username_clean.isdigit():
|
||||
return int(username_clean)
|
||||
except Exception as e:
|
||||
logger.error("Error resolving username %s: %s", username, e)
|
||||
except Exception as exc:
|
||||
logger.error("Error resolving username %s: %s", username, exc)
|
||||
|
||||
return None
|
||||
|
||||
@@ -62,8 +71,8 @@ async def _is_creator(message: Message) -> bool:
|
||||
for admin in admins:
|
||||
if admin.status == "creator" and admin.user.id == message.from_user.id:
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error("Error checking creator: %s", e)
|
||||
except Exception as exc:
|
||||
logger.error("Error checking creator: %s", exc)
|
||||
return False
|
||||
|
||||
|
||||
@@ -76,10 +85,18 @@ async def cmd_ai(message: Message):
|
||||
user_id = message.from_user.id
|
||||
chat_id = message.chat.id
|
||||
|
||||
await save_chat_user(user_id, chat_id, message.from_user.username, message.from_user.full_name or "")
|
||||
await save_chat_user(
|
||||
user_id,
|
||||
chat_id,
|
||||
message.from_user.username,
|
||||
message.from_user.full_name or "",
|
||||
)
|
||||
|
||||
if await is_ai_blocked(user_id, chat_id):
|
||||
await message.answer("Тебе временно недоступен AI. Обратись к владельцу чата.", parse_mode=None)
|
||||
await message.answer(
|
||||
"Тебе временно недоступен AI. Обратись к владельцу чата.",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
|
||||
text_parts = message.text.split(maxsplit=1)
|
||||
@@ -88,12 +105,21 @@ async def cmd_ai(message: Message):
|
||||
if message.reply_to_message:
|
||||
target_text = message.reply_to_message.text or message.reply_to_message.caption
|
||||
if not target_text:
|
||||
await message.answer("Могу работать только с текстовыми сообщениями.", parse_mode=None)
|
||||
await message.answer(
|
||||
"Могу работать только с текстовыми сообщениями.",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
prompt = f"Проанализируй это сообщение:\n\n{target_text}"
|
||||
context_text = target_text
|
||||
elif message.forward_from and (message.forward_from.text or message.forward_from.caption):
|
||||
target_text = message.forward_from.text or message.forward_from.caption
|
||||
elif message.forward_origin:
|
||||
target_text = message.text or message.caption
|
||||
if not target_text:
|
||||
await message.answer(
|
||||
"Могу работать только с текстовыми сообщениями.",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
prompt = f"Проанализируй это сообщение:\n\n{target_text}"
|
||||
context_text = target_text
|
||||
elif has_direct_question:
|
||||
@@ -107,21 +133,26 @@ async def cmd_ai(message: Message):
|
||||
|
||||
context = await get_user_context(user_id, chat_id, AI_CONTEXT_LIMIT)
|
||||
|
||||
extra_system_content = None
|
||||
try:
|
||||
relevant = await find_relevant_summaries(user_id, chat_id, prompt, top_k=2)
|
||||
if relevant:
|
||||
summary_text = "Из прошлых диалогов:\n" + "\n---\n".join(relevant)
|
||||
context.insert(0, {"role": "system", "content": summary_text})
|
||||
except Exception as e:
|
||||
logger.error("Error fetching relevant summaries: %s", e)
|
||||
extra_system_content = "Из прошлых диалогов:\n" + "\n---\n".join(relevant)
|
||||
except Exception as exc:
|
||||
logger.error("Error fetching relevant summaries: %s", exc)
|
||||
|
||||
async def update_status(text: str):
|
||||
try:
|
||||
await status_msg.edit_text(text, parse_mode=None)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as exc:
|
||||
logger.debug("Status update failed: %s", exc)
|
||||
|
||||
response = await ask_ai(prompt, context, status_callback=update_status)
|
||||
response = await ask_ai(
|
||||
prompt,
|
||||
context,
|
||||
status_callback=update_status,
|
||||
extra_system_content=extra_system_content,
|
||||
)
|
||||
|
||||
if not response:
|
||||
response = "Не удалось получить ответ от AI."
|
||||
@@ -130,9 +161,14 @@ async def cmd_ai(message: Message):
|
||||
await add_context_message(user_id, chat_id, response, "assistant")
|
||||
|
||||
try:
|
||||
await status_msg.edit_text(response, parse_mode="HTML", disable_web_page_preview=True)
|
||||
await status_msg.edit_text(
|
||||
response, parse_mode="HTML", disable_web_page_preview=True
|
||||
)
|
||||
except Exception:
|
||||
await status_msg.edit_text(response, parse_mode=None, disable_web_page_preview=True)
|
||||
try:
|
||||
await status_msg.edit_text(response, parse_mode=None, disable_web_page_preview=True)
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to edit status message: %s", exc)
|
||||
|
||||
|
||||
async def _start_dialogue(message: Message):
|
||||
@@ -142,8 +178,10 @@ async def _start_dialogue(message: Message):
|
||||
dialogue = await get_or_create_dialogue(user_id, chat_id)
|
||||
|
||||
if not dialogue["is_active"]:
|
||||
remaining = int(dialogue["blocked_until"] - __import__("time").time()) if dialogue["blocked_until"] else 0
|
||||
mins = remaining // 60
|
||||
remaining = (
|
||||
int(dialogue["blocked_until"] - time.time()) if dialogue["blocked_until"] else 0
|
||||
)
|
||||
mins = max(0, remaining // 60)
|
||||
await message.answer(
|
||||
f"Твой лимит диалога исчерпан. Попробуй через {mins} мин.",
|
||||
parse_mode=None,
|
||||
@@ -182,10 +220,13 @@ async def cmd_aiuser(message: Message):
|
||||
parse_mode=None,
|
||||
)
|
||||
|
||||
msg = await message.answer("✅ Список отправлен в ЛС.", parse_mode=None)
|
||||
except Exception as e:
|
||||
logger.error("aiuser error: %s", e)
|
||||
await message.answer("Не удалось получить список пользователей. Возможно, у бота нет доступа.", parse_mode=None)
|
||||
await message.answer("✅ Список отправлен в ЛС.", parse_mode=None)
|
||||
except Exception as exc:
|
||||
logger.error("aiuser error: %s", exc)
|
||||
await message.answer(
|
||||
"Не удалось получить список пользователей. Возможно, у бота нет доступа.",
|
||||
parse_mode=None,
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("aiclear"))
|
||||
@@ -195,28 +236,49 @@ async def cmd_aiclear(message: Message):
|
||||
|
||||
try:
|
||||
context_messages = await get_user_context(user_id, chat_id, limit=50)
|
||||
cnt = len(context_messages) if context_messages else 0
|
||||
if context_messages and cnt >= 2:
|
||||
status_msg = await message.answer("Сохраняю выжимку диалога...", parse_mode=None)
|
||||
cnt = len(context_messages)
|
||||
if cnt >= 2:
|
||||
status_msg = await message.answer(
|
||||
"Сохраняю выжимку диалога...", parse_mode=None
|
||||
)
|
||||
summary = await generate_summary(context_messages)
|
||||
if summary:
|
||||
await save_summary_with_embedding(user_id, chat_id, summary)
|
||||
await clear_user_context(user_id, chat_id)
|
||||
await status_msg.edit_text("✅ Выжимка сохранена. Диалог очищен.", parse_mode=None)
|
||||
logger.info("aiclear | user=%d chat=%d context=%d summary=%d emb=saved", user_id, chat_id, cnt, len(summary))
|
||||
await status_msg.edit_text(
|
||||
"✅ Выжимка сохранена. Диалог очищен.", parse_mode=None
|
||||
)
|
||||
logger.info(
|
||||
"aiclear | user=%d chat=%d context=%d summary=%d",
|
||||
user_id,
|
||||
chat_id,
|
||||
cnt,
|
||||
len(summary),
|
||||
)
|
||||
else:
|
||||
logger.warning("aiclear | user=%d chat=%d context=%d summary=None", user_id, chat_id, cnt)
|
||||
logger.warning(
|
||||
"aiclear | user=%d chat=%d context=%d summary=None",
|
||||
user_id,
|
||||
chat_id,
|
||||
cnt,
|
||||
)
|
||||
else:
|
||||
logger.warning("aiclear | user=%d chat=%d not enough context (%d < 2)", user_id, chat_id, cnt)
|
||||
except Exception as e:
|
||||
logger.error("aiclear summary error: %s", e)
|
||||
logger.warning(
|
||||
"aiclear | user=%d chat=%d not enough context (%d < 2)",
|
||||
user_id,
|
||||
chat_id,
|
||||
cnt,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error("aiclear summary error: %s", exc)
|
||||
|
||||
await clear_dialogue(user_id, chat_id)
|
||||
|
||||
from bot.utils.database import unblock_user_from_ai
|
||||
await unblock_user_from_ai(user_id, chat_id)
|
||||
|
||||
await message.answer("✅ Диалог очищен. Можешь начать новый.", parse_mode=None)
|
||||
await message.answer(
|
||||
"✅ Диалог очищен. Можешь начать новый.",
|
||||
parse_mode=None,
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("aino"))
|
||||
@@ -226,16 +288,30 @@ async def cmd_aino(message: Message):
|
||||
|
||||
username = extract_username(message.text)
|
||||
if not username:
|
||||
await message.answer("Укажи пользователя: /aino @username или /aino id<UID>", parse_mode=None)
|
||||
await message.answer(
|
||||
"Укажи пользователя: /aino @username или /aino id<UID>",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
|
||||
target_user_id = await get_user_id_by_username(message, username)
|
||||
if not target_user_id:
|
||||
await message.answer(f"Не удалось найти пользователя {username}.", parse_mode=None)
|
||||
await message.answer(
|
||||
f"Не удалось найти пользователя {username}.",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
|
||||
await block_user_from_ai(target_user_id, message.chat.id, message.from_user.id, AI_BLOCK_DEFAULT_DURATION)
|
||||
await message.answer(f"Пользователь {username} заблокирован от AI на 24 часа.", parse_mode=None)
|
||||
await block_user_from_ai(
|
||||
target_user_id,
|
||||
message.chat.id,
|
||||
message.from_user.id,
|
||||
AI_BLOCK_DEFAULT_DURATION,
|
||||
)
|
||||
await message.answer(
|
||||
f"Пользователь {username} заблокирован от AI на 24 часа.",
|
||||
parse_mode=None,
|
||||
)
|
||||
|
||||
|
||||
@router.message(Command("aiyes"))
|
||||
@@ -245,16 +321,28 @@ async def cmd_aiyes(message: Message):
|
||||
|
||||
username = extract_username(message.text)
|
||||
if not username:
|
||||
await message.answer("Укажи пользователя: /aiyes @username или /aiyes id<UID>", parse_mode=None)
|
||||
await message.answer(
|
||||
"Укажи пользователя: /aiyes @username или /aiyes id<UID>",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
|
||||
target_user_id = await get_user_id_by_username(message, username)
|
||||
if not target_user_id:
|
||||
await message.answer(f"Не удалось найти пользователя {username}.", parse_mode=None)
|
||||
await message.answer(
|
||||
f"Не удалось найти пользователя {username}.",
|
||||
parse_mode=None,
|
||||
)
|
||||
return
|
||||
|
||||
unblocked = await unblock_user_from_ai(target_user_id, message.chat.id)
|
||||
if unblocked:
|
||||
await message.answer(f"Пользователь {username} разблокирован для AI.", parse_mode=None)
|
||||
await message.answer(
|
||||
f"Пользователь {username} разблокирован для AI.",
|
||||
parse_mode=None,
|
||||
)
|
||||
else:
|
||||
await message.answer(f"Пользователь {username} не был заблокирован от AI.", parse_mode=None)
|
||||
await message.answer(
|
||||
f"Пользователь {username} не был заблокирован от AI.",
|
||||
parse_mode=None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user