Spaces:
Runtime error
Runtime error
Upload 12 files
Browse files- bot.py +187 -0
- config.py +48 -0
- database.py +192 -0
- handlers __init__.py +2 -0
- handlers chat.py +435 -0
- handlers commands.py +55 -0
- middlewares __init__.py +1 -0
- middlewares owner.py +26 -0
- service __init__.py +2 -0
- services glm.py +76 -0
- utils __init__.py +0 -0
- utils helpers.py +17 -0
bot.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import ssl
|
| 5 |
+
import sys
|
| 6 |
+
|
| 7 |
+
import aiohttp
|
| 8 |
+
import certifi
|
| 9 |
+
from aiohttp import web
|
| 10 |
+
from aiogram import Bot, Dispatcher
|
| 11 |
+
from aiogram.enums import ParseMode
|
| 12 |
+
from aiogram.webhook.aiohttp_server import SimpleRequestHandler, setup_application
|
| 13 |
+
|
| 14 |
+
from config import config
|
| 15 |
+
from database import db
|
| 16 |
+
from handlers import commands_router, chat_router
|
| 17 |
+
from middlewares import OwnerMiddleware
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 21 |
+
# BLOCK 0: SSL-патчи для HF Spaces (certifi + proxy)
|
| 22 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 23 |
+
# HF Spaces не имеет системных CA-сертификатов.
|
| 24 |
+
# Патчи применяются как side-effect при импорте.
|
| 25 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 26 |
+
|
| 27 |
+
custom_ssl = ssl.create_default_context(cafile=certifi.where())
|
| 28 |
+
custom_ssl.check_hostname = True
|
| 29 |
+
custom_ssl.verify_mode = ssl.CERT_REQUIRED
|
| 30 |
+
|
| 31 |
+
_orig_tcp_init = aiohttp.TCPConnector.__init__
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _patched_tcp_init(self, *args, **kwargs):
|
| 35 |
+
if kwargs.get("ssl") is not False:
|
| 36 |
+
kwargs["ssl"] = custom_ssl
|
| 37 |
+
_orig_tcp_init(self, *args, **kwargs)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
aiohttp.TCPConnector.__init__ = _patched_tcp_init
|
| 41 |
+
|
| 42 |
+
_orig_session_init = aiohttp.ClientSession.__init__
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def _patched_session_init(self, *args, **kwargs):
|
| 46 |
+
kwargs["trust_env"] = True
|
| 47 |
+
_orig_session_init(self, *args, **kwargs)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
aiohttp.ClientSession.__init__ = _patched_session_init
|
| 51 |
+
|
| 52 |
+
_orig_request = aiohttp.ClientSession._request
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
async def _patched_request(self, method, url, *args, **kwargs):
|
| 56 |
+
proxy_server = os.getenv("TELEGRAM_API_SERVER")
|
| 57 |
+
if proxy_server and "api.telegram.org" in str(url):
|
| 58 |
+
proxy_server = proxy_server.strip().rstrip("/")
|
| 59 |
+
str_url = str(url).replace("https://api.telegram.org", proxy_server)
|
| 60 |
+
logging.info("🔀 Переадресация aiogram через прокси ➡️ %s", str_url)
|
| 61 |
+
url = str_url
|
| 62 |
+
return await _orig_request(self, method, url, *args, **kwargs)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
aiohttp.ClientSession._request = _patched_request
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 69 |
+
# BLOCK 1: Логирование
|
| 70 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 71 |
+
|
| 72 |
+
def setup_logging() -> None:
|
| 73 |
+
logging.basicConfig(
|
| 74 |
+
level=logging.INFO,
|
| 75 |
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 76 |
+
stream=sys.stdout,
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 81 |
+
# BLOCK 2: Инициализация бота и диспетчера
|
| 82 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 83 |
+
|
| 84 |
+
bot = Bot(token=config.BOT_TOKEN, parse_mode=ParseMode.HTML)
|
| 85 |
+
dp = Dispatcher()
|
| 86 |
+
|
| 87 |
+
dp.message.middleware(OwnerMiddleware())
|
| 88 |
+
dp.include_router(commands_router)
|
| 89 |
+
dp.include_router(chat_router)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 93 |
+
# BLOCK 3: HTTP Handlers
|
| 94 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 95 |
+
|
| 96 |
+
@web.middleware
|
| 97 |
+
async def hf_logging_middleware(request, handler):
|
| 98 |
+
return await handler(request)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
async def health_check(request: web.Request) -> web.Response:
|
| 102 |
+
"""HF Spaces Health Check — обязательно отвечать 200 на '/'."""
|
| 103 |
+
return web.Response(text="🚀 GLM Bot РАБОТАЕТ!")
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 107 |
+
# BLOCK 4: Lifecycle hooks
|
| 108 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 109 |
+
|
| 110 |
+
async def on_startup(app: web.Application) -> None:
|
| 111 |
+
logger = logging.getLogger(__name__)
|
| 112 |
+
await db.connect()
|
| 113 |
+
logger.info("Database connected")
|
| 114 |
+
|
| 115 |
+
space_host = config.SPACE_HOST or os.getenv("SPACE_HOST", "")
|
| 116 |
+
if space_host:
|
| 117 |
+
full_webhook_link = f"https://{space_host.strip()}{config.WEBHOOK_PATH}"
|
| 118 |
+
for attempt in range(5):
|
| 119 |
+
try:
|
| 120 |
+
await bot.set_webhook(
|
| 121 |
+
url=full_webhook_link,
|
| 122 |
+
drop_pending_updates=True,
|
| 123 |
+
request_timeout=30,
|
| 124 |
+
)
|
| 125 |
+
logger.info("✅ Webhook установлен: %s", full_webhook_link)
|
| 126 |
+
break
|
| 127 |
+
except Exception as e:
|
| 128 |
+
logger.warning(
|
| 129 |
+
"⚠️ Попытка %d/5 установки webhook: %s", attempt + 1, e
|
| 130 |
+
)
|
| 131 |
+
await asyncio.sleep(5)
|
| 132 |
+
else:
|
| 133 |
+
logger.warning("⚠️ SPACE_HOST не задан, webhook не установлен!")
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
async def on_shutdown(app: web.Application) -> None:
|
| 137 |
+
logger = logging.getLogger(__name__)
|
| 138 |
+
logger.info("🛑 Shutdown начат...")
|
| 139 |
+
|
| 140 |
+
try:
|
| 141 |
+
await bot.delete_webhook(drop_pending_updates=True)
|
| 142 |
+
logger.info("Webhook удалён")
|
| 143 |
+
except Exception as e:
|
| 144 |
+
logger.warning("Ошибка при удалении webhook: %s", e)
|
| 145 |
+
|
| 146 |
+
try:
|
| 147 |
+
await dp.storage.close()
|
| 148 |
+
await bot.session.close()
|
| 149 |
+
logger.info("Bot session закрыт")
|
| 150 |
+
except Exception as e:
|
| 151 |
+
logger.warning("Ошибка при закрытии сессии бота: %s", e)
|
| 152 |
+
|
| 153 |
+
try:
|
| 154 |
+
await db.disconnect()
|
| 155 |
+
logger.info("Database disconnected")
|
| 156 |
+
except Exception as e:
|
| 157 |
+
logger.warning("Ошибка при отключении БД: %s", e)
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 161 |
+
# BLOCK 5: Main
|
| 162 |
+
# ═══════════════════════════════════════════════════════════════════
|
| 163 |
+
|
| 164 |
+
def main() -> None:
|
| 165 |
+
setup_logging()
|
| 166 |
+
logger = logging.getLogger(__name__)
|
| 167 |
+
logger.info("Starting HF Spaces bot...")
|
| 168 |
+
|
| 169 |
+
app = web.Application(middlewares=[hf_logging_middleware])
|
| 170 |
+
app.router.add_get("/", health_check)
|
| 171 |
+
|
| 172 |
+
webhook_requests_handler = SimpleRequestHandler(dispatcher=dp, bot=bot)
|
| 173 |
+
webhook_requests_handler.register(app, path=config.WEBHOOK_PATH)
|
| 174 |
+
setup_application(app, dp, bot=bot)
|
| 175 |
+
|
| 176 |
+
app.on_startup.append(on_startup)
|
| 177 |
+
app.on_shutdown.append(on_shutdown)
|
| 178 |
+
|
| 179 |
+
# HF Spaces требует: host='0.0.0.0', port=7860
|
| 180 |
+
port = int(os.environ.get("PORT", 7860))
|
| 181 |
+
logger.info("🚀 Запуск сервера на %s:%d...", "0.0.0.0", port)
|
| 182 |
+
web.run_app(app, host="0.0.0.0", port=port)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
if __name__ == "__main__":
|
| 186 |
+
main()
|
| 187 |
+
|
config.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class Config:
|
| 10 |
+
BOT_TOKEN: str = os.getenv("BOT_TOKEN", "")
|
| 11 |
+
OWNER_ID: int = int(os.getenv("OWNER_ID", "0") or "0")
|
| 12 |
+
DATABASE_URL: str = os.getenv("DATABASE_URL", "")
|
| 13 |
+
NVIDIA_API_KEY: str = os.getenv("NVIDIA_API_KEY", "")
|
| 14 |
+
NVIDIA_BASE_URL: str = "https://integrate.api.nvidia.com/v1"
|
| 15 |
+
MODEL_NAME: str = "z-ai/glm-5.1"
|
| 16 |
+
MAX_HISTORY: int = 30
|
| 17 |
+
MAX_MESSAGE_LENGTH: int = 4096
|
| 18 |
+
SUMMARIZE_THRESHOLD: int = 30 # Суммаризировать когда сообщений > N
|
| 19 |
+
SUMMARY_MAX_TOKENS: int = 512
|
| 20 |
+
GLM_TEMPERATURE: float = 0.8
|
| 21 |
+
GLM_TOP_P: float = 0.95
|
| 22 |
+
GLM_FREQUENCY_PENALTY: float = 0.2
|
| 23 |
+
GLM_PRESENCE_PENALTY: float = 0.1
|
| 24 |
+
GLM_MAX_TOKENS: int = 8192
|
| 25 |
+
WEBHOOK_PATH: str = "/webhook"
|
| 26 |
+
SPACE_HOST: str = os.getenv("SPACE_HOST", "")
|
| 27 |
+
|
| 28 |
+
def __post_init__(self) -> None:
|
| 29 |
+
# Neon and some providers use postgres:// which asyncpg does not accept
|
| 30 |
+
if self.DATABASE_URL.startswith("postgres://"):
|
| 31 |
+
self.DATABASE_URL = self.DATABASE_URL.replace(
|
| 32 |
+
"postgres://", "postgresql://", 1
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
def validate(self) -> None:
|
| 36 |
+
if not self.BOT_TOKEN:
|
| 37 |
+
raise ValueError("BOT_TOKEN is required")
|
| 38 |
+
if not self.OWNER_ID:
|
| 39 |
+
raise ValueError("OWNER_ID is required")
|
| 40 |
+
if not self.DATABASE_URL:
|
| 41 |
+
raise ValueError("DATABASE_URL is required")
|
| 42 |
+
if not self.NVIDIA_API_KEY:
|
| 43 |
+
raise ValueError("NVIDIA_API_KEY is required")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
config = Config()
|
| 47 |
+
config.validate()
|
| 48 |
+
|
database.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncpg
|
| 2 |
+
import logging
|
| 3 |
+
from typing import Optional, List, Dict, Any
|
| 4 |
+
from config import config
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Database:
|
| 10 |
+
def __init__(self) -> None:
|
| 11 |
+
self.pool: Optional[asyncpg.Pool] = None
|
| 12 |
+
|
| 13 |
+
async def connect(self) -> None:
|
| 14 |
+
# Render free tier: 512 MB RAM, keep pool small
|
| 15 |
+
self.pool = await asyncpg.create_pool(
|
| 16 |
+
dsn=config.DATABASE_URL,
|
| 17 |
+
min_size=1,
|
| 18 |
+
max_size=3,
|
| 19 |
+
command_timeout=60,
|
| 20 |
+
)
|
| 21 |
+
logger.info("Database pool created (max_size=3)")
|
| 22 |
+
await self._create_tables()
|
| 23 |
+
|
| 24 |
+
async def disconnect(self) -> None:
|
| 25 |
+
if self.pool:
|
| 26 |
+
await self.pool.close()
|
| 27 |
+
logger.info("Database pool closed")
|
| 28 |
+
|
| 29 |
+
def _acquire(self):
|
| 30 |
+
if self.pool is None:
|
| 31 |
+
raise RuntimeError("Database not connected. Call connect() first.")
|
| 32 |
+
return self.pool.acquire()
|
| 33 |
+
|
| 34 |
+
async def _create_tables(self) -> None:
|
| 35 |
+
async with self._acquire() as conn:
|
| 36 |
+
await conn.execute("""
|
| 37 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 38 |
+
id BIGINT PRIMARY KEY,
|
| 39 |
+
username VARCHAR(255),
|
| 40 |
+
first_name VARCHAR(255),
|
| 41 |
+
last_name VARCHAR(255),
|
| 42 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 43 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 44 |
+
)
|
| 45 |
+
""")
|
| 46 |
+
await conn.execute("""
|
| 47 |
+
CREATE TABLE IF NOT EXISTS messages (
|
| 48 |
+
id SERIAL PRIMARY KEY,
|
| 49 |
+
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
| 50 |
+
role VARCHAR(20) NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
|
| 51 |
+
content TEXT NOT NULL,
|
| 52 |
+
is_summarized BOOLEAN DEFAULT FALSE,
|
| 53 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 54 |
+
)
|
| 55 |
+
""")
|
| 56 |
+
await conn.execute("""
|
| 57 |
+
CREATE INDEX IF NOT EXISTS idx_messages_user_id_created_at
|
| 58 |
+
ON messages(user_id, created_at DESC)
|
| 59 |
+
""")
|
| 60 |
+
await conn.execute("""
|
| 61 |
+
CREATE INDEX IF NOT EXISTS idx_messages_user_id_summarized
|
| 62 |
+
ON messages(user_id, is_summarized, created_at DESC)
|
| 63 |
+
""")
|
| 64 |
+
await conn.execute("""
|
| 65 |
+
CREATE TABLE IF NOT EXISTS summaries (
|
| 66 |
+
id SERIAL PRIMARY KEY,
|
| 67 |
+
user_id BIGINT NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
| 68 |
+
summary TEXT NOT NULL,
|
| 69 |
+
message_count INTEGER NOT NULL DEFAULT 0,
|
| 70 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 71 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 72 |
+
)
|
| 73 |
+
""")
|
| 74 |
+
|
| 75 |
+
logger.info("Database tables created/verified")
|
| 76 |
+
|
| 77 |
+
async def upsert_user(
|
| 78 |
+
self,
|
| 79 |
+
user_id: int,
|
| 80 |
+
username: Optional[str],
|
| 81 |
+
first_name: Optional[str],
|
| 82 |
+
last_name: Optional[str],
|
| 83 |
+
) -> None:
|
| 84 |
+
async with self._acquire() as conn:
|
| 85 |
+
await conn.execute("""
|
| 86 |
+
INSERT INTO users (id, username, first_name, last_name)
|
| 87 |
+
VALUES ($1, $2, $3, $4)
|
| 88 |
+
ON CONFLICT (id) DO UPDATE SET
|
| 89 |
+
username = EXCLUDED.username,
|
| 90 |
+
first_name = EXCLUDED.first_name,
|
| 91 |
+
last_name = EXCLUDED.last_name,
|
| 92 |
+
updated_at = NOW()
|
| 93 |
+
""", user_id, username, first_name, last_name)
|
| 94 |
+
|
| 95 |
+
async def save_message(self, user_id: int, role: str, content: str) -> None:
|
| 96 |
+
async with self._acquire() as conn:
|
| 97 |
+
await conn.execute("""
|
| 98 |
+
INSERT INTO messages (user_id, role, content)
|
| 99 |
+
VALUES ($1, $2, $3)
|
| 100 |
+
""", user_id, role, content)
|
| 101 |
+
|
| 102 |
+
async def get_messages(self, user_id: int, limit: int = 30) -> List[Dict[str, Any]]:
|
| 103 |
+
async with self._acquire() as conn:
|
| 104 |
+
rows = await conn.fetch("""
|
| 105 |
+
SELECT role, content, created_at
|
| 106 |
+
FROM messages
|
| 107 |
+
WHERE user_id = $1 AND is_summarized = FALSE
|
| 108 |
+
ORDER BY created_at DESC
|
| 109 |
+
LIMIT $2
|
| 110 |
+
""", user_id, limit)
|
| 111 |
+
# Reverse to chronological order for the LLM
|
| 112 |
+
return [
|
| 113 |
+
{"role": r["role"], "content": r["content"], "created_at": r["created_at"]}
|
| 114 |
+
for r in reversed(rows)
|
| 115 |
+
]
|
| 116 |
+
|
| 117 |
+
async def get_summary(self, user_id: int) -> Optional[str]:
|
| 118 |
+
async with self._acquire() as conn:
|
| 119 |
+
row = await conn.fetchrow("""
|
| 120 |
+
SELECT summary FROM summaries WHERE user_id = $1
|
| 121 |
+
""", user_id)
|
| 122 |
+
return row["summary"] if row else None
|
| 123 |
+
|
| 124 |
+
async def save_summary(self, user_id: int, summary: str, message_count: int) -> None:
|
| 125 |
+
async with self._acquire() as conn:
|
| 126 |
+
await conn.execute("""
|
| 127 |
+
INSERT INTO summaries (user_id, summary, message_count, updated_at)
|
| 128 |
+
VALUES ($1, $2, $3, NOW())
|
| 129 |
+
ON CONFLICT (user_id) DO UPDATE SET
|
| 130 |
+
summary = EXCLUDED.summary,
|
| 131 |
+
message_count = summaries.message_count + EXCLUDED.message_count,
|
| 132 |
+
updated_at = NOW()
|
| 133 |
+
""", user_id, summary, message_count)
|
| 134 |
+
|
| 135 |
+
async def mark_summarized(self, user_id: int, cutoff_id: int) -> None:
|
| 136 |
+
async with self._acquire() as conn:
|
| 137 |
+
await conn.execute("""
|
| 138 |
+
UPDATE messages
|
| 139 |
+
SET is_summarized = TRUE
|
| 140 |
+
WHERE user_id = $1 AND id <= $2
|
| 141 |
+
""", user_id, cutoff_id)
|
| 142 |
+
|
| 143 |
+
async def get_oldest_unsummarized(self, user_id: int, limit: int) -> List[Dict[str, Any]]:
|
| 144 |
+
async with self._acquire() as conn:
|
| 145 |
+
rows = await conn.fetch("""
|
| 146 |
+
SELECT id, role, content
|
| 147 |
+
FROM messages
|
| 148 |
+
WHERE user_id = $1 AND is_summarized = FALSE
|
| 149 |
+
ORDER BY created_at ASC
|
| 150 |
+
LIMIT $2
|
| 151 |
+
""", user_id, limit)
|
| 152 |
+
return [{"id": r["id"], "role": r["role"], "content": r["content"]} for r in rows]
|
| 153 |
+
|
| 154 |
+
async def count_unsummarized(self, user_id: int) -> int:
|
| 155 |
+
async with self._acquire() as conn:
|
| 156 |
+
return await conn.fetchval("""
|
| 157 |
+
SELECT COUNT(*) FROM messages
|
| 158 |
+
WHERE user_id = $1 AND is_summarized = FALSE
|
| 159 |
+
""", user_id) or 0
|
| 160 |
+
|
| 161 |
+
async def clear_history(self, user_id: int) -> int:
|
| 162 |
+
async with self._acquire() as conn:
|
| 163 |
+
result = await conn.execute("""
|
| 164 |
+
DELETE FROM messages WHERE user_id = $1
|
| 165 |
+
""", user_id)
|
| 166 |
+
await conn.execute("""
|
| 167 |
+
DELETE FROM summaries WHERE user_id = $1
|
| 168 |
+
""", user_id)
|
| 169 |
+
try:
|
| 170 |
+
count = int(result.split()[-1])
|
| 171 |
+
except (ValueError, IndexError):
|
| 172 |
+
count = 0
|
| 173 |
+
logger.info("Cleared %d messages and summary for user %s", count, user_id)
|
| 174 |
+
return count
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
async def get_stats(self, user_id: int) -> Dict[str, Any]:
|
| 178 |
+
async with self._acquire() as conn:
|
| 179 |
+
user_count = await conn.fetchval("SELECT COUNT(*) FROM users")
|
| 180 |
+
msg_count = await conn.fetchval(
|
| 181 |
+
"SELECT COUNT(*) FROM messages WHERE user_id = $1", user_id
|
| 182 |
+
)
|
| 183 |
+
total_msg_count = await conn.fetchval("SELECT COUNT(*) FROM messages")
|
| 184 |
+
return {
|
| 185 |
+
"total_users": user_count,
|
| 186 |
+
"user_messages": msg_count,
|
| 187 |
+
"total_messages": total_msg_count,
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
db = Database()
|
| 192 |
+
|
handlers __init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .commands import router as commands_router
|
| 2 |
+
from .chat import router as chat_router
|
handlers chat.py
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import logging
|
| 3 |
+
from aiogram import Router, types
|
| 4 |
+
from aiogram.exceptions import TelegramAPIError
|
| 5 |
+
from config import config
|
| 6 |
+
from database import db
|
| 7 |
+
from services.glm import glm_service
|
| 8 |
+
from utils.helpers import send_long_message
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
router = Router()
|
| 12 |
+
|
| 13 |
+
SYSTEM_PROMPT = {
|
| 14 |
+
"role": "system",
|
| 15 |
+
"content": (
|
| 16 |
+
"ТЫ РАБОТАЕШЬ КАК SENIOR STAFF PYTHON ENGINEER И ACTING CTO.\n\n"
|
| 17 |
+
"ТЫ РАЗРАБАТЫВАЕШЬ PRODUCTION TELEGRAM AI SAAS.\n\n"
|
| 18 |
+
"ТВОЯ ГЛАВНАЯ ЗАДАЧА:\n\n"
|
| 19 |
+
"Реализовывать новые функции, исправлять баги и развивать архитектуру без нарушения существующей бизнес-логики.\n\n"
|
| 20 |
+
"============================================================\n"
|
| 21 |
+
"ДОЛГОВРЕМЕННАЯ ПАМЯТЬ И КОНТЕКСТ\n"
|
| 22 |
+
"============================================================\n\n"
|
| 23 |
+
"У тебя есть доступ к долговременной памяти через Neon PostgreSQL.\n"
|
| 24 |
+
"Ты получаешь два источника контекста:\n"
|
| 25 |
+
"1. [Контекст предыдущих диалогов] — суммаризированная история старых разговоров.\n"
|
| 26 |
+
"2. Последние сообщения текущего диалога — непосредственная история.\n\n"
|
| 27 |
+
"Правила работы с памятью:\n"
|
| 28 |
+
"- Используй summary для понимания долгосрочных предпочтений пользователя, его проекта, стека технологий.\n"
|
| 29 |
+
"- Ссылайся на предыдущие решения и обсуждения, если они релевантны текущему запросу.\n"
|
| 30 |
+
"- Не повторяй информацию из summary, если пользователь уже знает её.\n"
|
| 31 |
+
"- Если пользователь просит продолжить работу — используй контекст для восстановления состояния.\n"
|
| 32 |
+
"- Помни: история сохраняется между сессиями. Бот перезапускается, но память остаётся.\n\n"
|
| 33 |
+
"============================================================\n"
|
| 34 |
+
"ОБЯЗАТЕЛЬНЫЙ ПРОЦЕСС РАБОТЫ\n"
|
| 35 |
+
"============================================================\n\n"
|
| 36 |
+
"Перед написанием кода всегда выполняй:\n\n"
|
| 37 |
+
"1. Анализ задачи.\n"
|
| 38 |
+
"2. Анализ архитектуры.\n"
|
| 39 |
+
"3. Поиск точки интеграции.\n"
|
| 40 |
+
"4. Поиск зависимостей.\n"
|
| 41 |
+
"5. Анализ побочных эффектов.\n"
|
| 42 |
+
"6. Только после этого предлагай изменения.\n\n"
|
| 43 |
+
"Никогда не начинай писать код сразу.\n\n"
|
| 44 |
+
"============================================================\n"
|
| 45 |
+
"КРИТИЧЕСКИЕ ЗАПРЕТЫ\n"
|
| 46 |
+
"============================================================\n\n"
|
| 47 |
+
"Запрещено:\n\n"
|
| 48 |
+
"- ломать существующую архитектуру;\n"
|
| 49 |
+
"- удалять существующую бизнес-логику;\n"
|
| 50 |
+
"- менять FSM без необходимости;\n"
|
| 51 |
+
"- менять маршрутизацию бота без необходимости;\n"
|
| 52 |
+
"- менять структуру БД без необходимости;\n"
|
| 53 |
+
"- менять OpenAI-интеграцию без необходимости;\n"
|
| 54 |
+
"- менять существующие API без необходимости;\n"
|
| 55 |
+
"- менять рабочий код ради рефакторинга.\n\n"
|
| 56 |
+
"Если код уже работает:\n\n"
|
| 57 |
+
"СОХРАНИТЬ ЕГО.\n\n"
|
| 58 |
+
"Менять только то, что требуется задачей.\n\n"
|
| 59 |
+
"============================================================\n"
|
| 60 |
+
"ПРИНЦИП МИНИМАЛЬНЫХ ИЗМЕНЕНИЙ\n"
|
| 61 |
+
"============================================================\n\n"
|
| 62 |
+
"Предпочитать:\n\n"
|
| 63 |
+
"- локальные патчи;\n"
|
| 64 |
+
"- локальные функции;\n"
|
| 65 |
+
"- локальные классы;\n"
|
| 66 |
+
"- локальные интеграции.\n\n"
|
| 67 |
+
"Не переписывать большие блоки ради маленькой задачи.\n\n"
|
| 68 |
+
"============================================================\n"
|
| 69 |
+
"ПРАВИЛО БОЛЬШИХ ФАЙЛОВ\n"
|
| 70 |
+
"============================================================\n\n"
|
| 71 |
+
"Файл может содержать:\n\n"
|
| 72 |
+
"5000+\n"
|
| 73 |
+
"7000+\n"
|
| 74 |
+
"10000+ строк.\n\n"
|
| 75 |
+
"Если изменение можно выполнить локально:\n\n"
|
| 76 |
+
"НЕ ВЫВОДИТЬ ВЕСЬ БЛОК.\n\n"
|
| 77 |
+
"НЕ ВЫВОДИТЬ ВЕСЬ ФАЙЛ.\n\n"
|
| 78 |
+
"НЕ ВЫВОДИТЬ ВЕСЬ КЛАСС.\n\n"
|
| 79 |
+
"Использовать PATCH MODE.\n\n"
|
| 80 |
+
"============================================================\n"
|
| 81 |
+
"PATCH MODE\n"
|
| 82 |
+
"============================================================\n\n"
|
| 83 |
+
"Если блок больше 200 строк:\n\n"
|
| 84 |
+
"НЕ ВЫВОДИТЬ ЕГО ПОЛНОСТЬЮ.\n\n"
|
| 85 |
+
"Вместо этого показывать:\n\n"
|
| 86 |
+
"PATCH 1/N\n\n"
|
| 87 |
+
"БЛОК:\n"
|
| 88 |
+
"ПОДБЛОК:\n\n"
|
| 89 |
+
"НАЙТИ:\n\n"
|
| 90 |
+
"<существующий код>\n\n"
|
| 91 |
+
"ДОБАВИТЬ ПОСЛЕ:\n\n"
|
| 92 |
+
"<новый код>\n\n"
|
| 93 |
+
"или\n\n"
|
| 94 |
+
"ЗАМЕНИТЬ:\n\n"
|
| 95 |
+
"<старый код>\n\n"
|
| 96 |
+
"НА:\n\n"
|
| 97 |
+
"<новый код>\n\n"
|
| 98 |
+
"Каждый патч должен быть независимым.\n\n"
|
| 99 |
+
"Каждый патч должен копироваться отдельно.\n\n"
|
| 100 |
+
"Размер одного патча желательно не более 150 строк.\n\n"
|
| 101 |
+
"============================================================\n"
|
| 102 |
+
"ГРАНИЦЫ БЛОКОВ\n"
|
| 103 |
+
"============================================================\n\n"
|
| 104 |
+
"Если в проекте существуют блоки вида:\n\n"
|
| 105 |
+
"# ============================================================\n"
|
| 106 |
+
"# BLOCK X\n"
|
| 107 |
+
"# ============================================================\n\n"
|
| 108 |
+
"или\n\n"
|
| 109 |
+
"# SUBBLOCK X.X\n\n"
|
| 110 |
+
"использовать их как ориентиры.\n\n"
|
| 111 |
+
"Всегда указывать:\n\n"
|
| 112 |
+
"БЛОК:\n"
|
| 113 |
+
"ПОДБЛОК:\n\n"
|
| 114 |
+
"для каждого изменения.\n\n"
|
| 115 |
+
"============================================================\n"
|
| 116 |
+
"ТРЕБОВАНИЯ К КОДУ\n"
|
| 117 |
+
"============================================================\n\n"
|
| 118 |
+
"Код должен быть:\n\n"
|
| 119 |
+
"- Production Ready;\n"
|
| 120 |
+
"- Python 3.11+;\n"
|
| 121 |
+
"- Async Safe;\n"
|
| 122 |
+
"- Thread Safe;\n"
|
| 123 |
+
"- читаемым;\n"
|
| 124 |
+
"- масштабируемым;\n"
|
| 125 |
+
"- совместимым с существующим проектом.\n\n"
|
| 126 |
+
"============================================================\n"
|
| 127 |
+
"ЗАПРЕЩЕНО В КОДЕ\n"
|
| 128 |
+
"============================================================\n\n"
|
| 129 |
+
"Запрещено:\n\n"
|
| 130 |
+
"TODO\n\n"
|
| 131 |
+
"FIXME\n\n"
|
| 132 |
+
"PLACEHOLDER\n\n"
|
| 133 |
+
"PASS\n\n"
|
| 134 |
+
"ваша логика здесь\n\n"
|
| 135 |
+
"псевдокод\n\n"
|
| 136 |
+
"сокращённые реализации\n\n"
|
| 137 |
+
"незавершённые функции\n\n"
|
| 138 |
+
"============================================================\n"
|
| 139 |
+
"ПОЛНОТА КОДА\n"
|
| 140 |
+
"============================================================\n\n"
|
| 141 |
+
"Если изменяется функция:\n\n"
|
| 142 |
+
"выводить функцию полностью.\n\n"
|
| 143 |
+
"Если изменяется класс:\n\n"
|
| 144 |
+
"выводить класс полностью.\n\n"
|
| 145 |
+
"Если изменяется маленький блок:\n\n"
|
| 146 |
+
"выводить блок полностью.\n\n"
|
| 147 |
+
"Если изменяется большой блок:\n\n"
|
| 148 |
+
"использовать PATCH MODE.\n\n"
|
| 149 |
+
"============================================================\n"
|
| 150 |
+
"БЕЗОПАСНОСТЬ\n"
|
| 151 |
+
"============================================================\n\n"
|
| 152 |
+
"Проверять:\n\n"
|
| 153 |
+
"- SQL Injection\n"
|
| 154 |
+
"- Prompt Injection\n"
|
| 155 |
+
"- Race Conditions\n"
|
| 156 |
+
"- Deadlocks\n"
|
| 157 |
+
"- Resource Leaks\n"
|
| 158 |
+
"- Memory Leaks\n"
|
| 159 |
+
"- Invalid User Input\n"
|
| 160 |
+
"- Unsafe Async Usage\n\n"
|
| 161 |
+
"============================================================\n"
|
| 162 |
+
"ПРОВЕРКА ПЕРЕД ОТВЕТОМ\n"
|
| 163 |
+
"============================================================\n\n"
|
| 164 |
+
"Перед выдачей решения проверить:\n\n"
|
| 165 |
+
"1. Не ломает ли решение текущую логику.\n"
|
| 166 |
+
"2. Не дублирует ли код.\n"
|
| 167 |
+
"3. Не создаёт ли новые зависимости.\n"
|
| 168 |
+
"4. Не нарушает ли архитектуру.\n"
|
| 169 |
+
"5. Не требует ли замены большого блока вместо локального патча.\n\n"
|
| 170 |
+
"============================================================\n"
|
| 171 |
+
"ФОРМАТ ОТВЕТА\n"
|
| 172 |
+
"============================================================\n\n"
|
| 173 |
+
"ВСЕГДА:\n\n"
|
| 174 |
+
"1. Анализ.\n"
|
| 175 |
+
"2. Что будет изменено.\n"
|
| 176 |
+
"3. Возможные риски.\n"
|
| 177 |
+
"4. PATCHES.\n"
|
| 178 |
+
"5. Инструкция по вставке.\n\n"
|
| 179 |
+
"Отвечай только на русском языке.\n\n"
|
| 180 |
+
"Никогда не сокращай код.\n"
|
| 181 |
+
"Никогда не пропускай важные части реализации.\n"
|
| 182 |
+
"Никогда не используй псевдокод.\n\n"
|
| 183 |
+
"============================================================\n"
|
| 184 |
+
"МЕЖМОДУЛЬНЫЙ АНАЛИЗ\n"
|
| 185 |
+
"============================================================\n\n"
|
| 186 |
+
"Перед изменением определить:\n\n"
|
| 187 |
+
"ПРЯМЫЕ ЗАВИСИМОСТИ:\n"
|
| 188 |
+
"- кто вызывает код\n\n"
|
| 189 |
+
"ОБРАТНЫЕ ЗАВИСИМОСТИ:\n"
|
| 190 |
+
"- кого вызывает код\n\n"
|
| 191 |
+
"ИМПОРТЫ:\n"
|
| 192 |
+
"- кто импортирует сущность\n\n"
|
| 193 |
+
"СВЯЗИ:\n"
|
| 194 |
+
"- БД\n"
|
| 195 |
+
"- FSM\n"
|
| 196 |
+
"- AI\n"
|
| 197 |
+
"- Memory\n"
|
| 198 |
+
"- Cache\n"
|
| 199 |
+
"- Middleware\n"
|
| 200 |
+
"- Scheduler\n"
|
| 201 |
+
"- Background Tasks\n\n"
|
| 202 |
+
"Вывести:\n\n"
|
| 203 |
+
"ЗАТРОНУТЫЕ ФАЙЛЫ\n\n"
|
| 204 |
+
"ПОТЕНЦИАЛЬНОЕ ВЛИЯНИЕ\n\n"
|
| 205 |
+
"УРОВЕНЬ РИСКА:\n\n"
|
| 206 |
+
"LOW\n"
|
| 207 |
+
"MEDIUM\n"
|
| 208 |
+
"HIGH\n"
|
| 209 |
+
"============================================================\n"
|
| 210 |
+
"РЕЖИМ НЕПОЛНОГО КОНТЕКСТА\n"
|
| 211 |
+
"============================================================\n\n"
|
| 212 |
+
"Если предоставлен не весь проект:\n\n"
|
| 213 |
+
"НЕ ДЕЛАТЬ ПРЕДПОЛОЖЕНИЙ.\n\n"
|
| 214 |
+
"НЕ ВЫДУМЫВАТЬ:\n\n"
|
| 215 |
+
"- существующие классы;\n"
|
| 216 |
+
"- существующие функции;\n"
|
| 217 |
+
"- существующие файлы;\n"
|
| 218 |
+
"- существующие импорты.\n\n"
|
| 219 |
+
"Если для безопасной реализации не хватает контекста:\n\n"
|
| 220 |
+
"УКАЗАТЬ:\n\n"
|
| 221 |
+
"ТРЕБУЕТСЯ ФАЙЛ:\n\n"
|
| 222 |
+
"- path/to/file.py\n\n"
|
| 223 |
+
"ПРИЧИНА:\n\n"
|
| 224 |
+
"Для проверки зависимостей и безопасной интеграции.\n\n"
|
| 225 |
+
"============================================================\n"
|
| 226 |
+
"ЗАЩИТА ОТ ГАЛЛЮЦИНАЦИЙ\n"
|
| 227 |
+
"============================================================\n\n"
|
| 228 |
+
"Запрещено:\n\n"
|
| 229 |
+
"- придумывать существующие методы;\n"
|
| 230 |
+
"- придумывать существующие классы;\n"
|
| 231 |
+
"- придумывать существующие таблицы БД;\n"
|
| 232 |
+
"- придумывать существующие поля БД;\n"
|
| 233 |
+
"- придумывать существующие API.\n\n"
|
| 234 |
+
"Если сущность не показана в предоставленном коде:\n\n"
|
| 235 |
+
"считать её неизвестной.\n\n"
|
| 236 |
+
"При необходимости запросить дополнительный код.\n"
|
| 237 |
+
"============================================================\n"
|
| 238 |
+
"ПРАВИЛО EXISTING IMPLEMENTATION FIRST\n"
|
| 239 |
+
"============================================================\n\n"
|
| 240 |
+
"Перед реализацией любой функции обязательно проверить:\n\n"
|
| 241 |
+
"1. Существует ли уже похожая реализация.\n"
|
| 242 |
+
"2. Существует ли частичная реализация.\n"
|
| 243 |
+
"3. Существует ли незавершённая реализация.\n"
|
| 244 |
+
"4. Существует ли старый код, который можно расширить.\n\n"
|
| 245 |
+
"Запрещено создавать:\n\n"
|
| 246 |
+
"- второй MemoryManager;\n"
|
| 247 |
+
"- второй AI Service;\n"
|
| 248 |
+
"- второй Repository;\n"
|
| 249 |
+
"- второй FSM Flow;\n"
|
| 250 |
+
"- второй Cache Layer;\n"
|
| 251 |
+
"- второй RAG Layer.\n\n"
|
| 252 |
+
"Если функциональность уже существует:\n\n"
|
| 253 |
+
"РАСШИРЯТЬ СУЩЕСТВУЮЩУЮ.\n\n"
|
| 254 |
+
"Не создавать дубликаты.\n"
|
| 255 |
+
"============================================================\n"
|
| 256 |
+
"РАБОТА ПО ФАЗАМ ПРОЕКТА\n"
|
| 257 |
+
"============================================================\n\n"
|
| 258 |
+
"Проект развивается по фазам.\n\n"
|
| 259 |
+
"Перед началом работы определить:\n\n"
|
| 260 |
+
"ТЕКУЩАЯ ФАЗА:\n"
|
| 261 |
+
"- B1\n"
|
| 262 |
+
"- B2\n"
|
| 263 |
+
"- B3\n"
|
| 264 |
+
"- B4\n"
|
| 265 |
+
"- B5\n"
|
| 266 |
+
"- B6\n"
|
| 267 |
+
"- B7\n"
|
| 268 |
+
"- C1\n"
|
| 269 |
+
"- C2\n"
|
| 270 |
+
"- и т.д.\n\n"
|
| 271 |
+
"Для каждой задачи определить:\n\n"
|
| 272 |
+
"1. Что уже реализовано.\n"
|
| 273 |
+
"2. Что закрыто.\n"
|
| 274 |
+
"3. Что находится в работе.\n"
|
| 275 |
+
"4. Что запрещено менять.\n\n"
|
| 276 |
+
"Запрещено:\n\n"
|
| 277 |
+
"- повторно реализовывать завершённые фазы;\n"
|
| 278 |
+
"- удалять код завершённых фаз;\n"
|
| 279 |
+
"- изменять код завершённых фаз без необходимости.\n\n"
|
| 280 |
+
"Если реализация относится к другой фазе:\n\n"
|
| 281 |
+
"СНАЧАЛА сообщить об этом.\n"
|
| 282 |
+
"============================================================\n"
|
| 283 |
+
"ПРЕДВАРИТЕЛЬНЫЙ АУДИТ\n"
|
| 284 |
+
"============================================================\n\n"
|
| 285 |
+
"Перед выдачей патча выполнить аудит:\n\n"
|
| 286 |
+
"- архитектуры;\n"
|
| 287 |
+
"- зависимостей;\n"
|
| 288 |
+
"- интеграций;\n"
|
| 289 |
+
"- БД;\n"
|
| 290 |
+
"- памяти;\n"
|
| 291 |
+
"- AI слоя;\n"
|
| 292 |
+
"- FSM;\n"
|
| 293 |
+
"- middleware;\n"
|
| 294 |
+
"- rate limits;\n"
|
| 295 |
+
"- кеширования.\n\n"
|
| 296 |
+
"Сначала вывести:\n\n"
|
| 297 |
+
"ROOT CAUSE\n\n"
|
| 298 |
+
"Затем:\n\n"
|
| 299 |
+
"АРХИТЕКТУРНОЕ РЕШЕНИЕ\n\n"
|
| 300 |
+
"И только потом патчи.\n"
|
| 301 |
+
"============================================================\n"
|
| 302 |
+
"МОНОЛИТНЫЕ ФАЙЛЫ\n"
|
| 303 |
+
"============================================================\n\n"
|
| 304 |
+
"Если файл больше 2000 строк:\n\n"
|
| 305 |
+
"Сначала определить:\n\n"
|
| 306 |
+
"- BLOCK\n"
|
| 307 |
+
"- SUBBLOCK\n\n"
|
| 308 |
+
"Запрещено:\n\n"
|
| 309 |
+
"- переписывать файл полностью;\n"
|
| 310 |
+
"- делать рефакторинг всего файла;\n"
|
| 311 |
+
"- менять порядок блоков.\n\n"
|
| 312 |
+
"Разрешено:\n\n"
|
| 313 |
+
"- локальные вставки;\n"
|
| 314 |
+
"- локальные замены;\n"
|
| 315 |
+
"- локальные фиксы.\n\n"
|
| 316 |
+
"Всегда сохранять существующую структуру.\n"
|
| 317 |
+
"============================================================\n"
|
| 318 |
+
"ПОИСК ТОЧКИ ИНТЕГРАЦИИ\n"
|
| 319 |
+
"============================================================\n\n"
|
| 320 |
+
"Перед написанием нового кода определить:\n\n"
|
| 321 |
+
"1. Где должна находиться реализация.\n"
|
| 322 |
+
"2. Какой существующий блок отвечает за данную функцию.\n"
|
| 323 |
+
"3. Можно ли встроить решение в существующий код.\n\n"
|
| 324 |
+
"Запрещено создавать новый модуль, если задача решается внутри существующего.\n\n"
|
| 325 |
+
"Сначала искать точку интеграции.\n\n"
|
| 326 |
+
"Потом писать код."
|
| 327 |
+
)
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
async def _maybe_summarize(user_id: int) -> None:
|
| 332 |
+
"""Фоновая суммаризация старой истории."""
|
| 333 |
+
try:
|
| 334 |
+
count = await db.count_unsummarized(user_id)
|
| 335 |
+
threshold = config.SUMMARIZE_THRESHOLD
|
| 336 |
+
keep_recent = 10
|
| 337 |
+
|
| 338 |
+
if count <= threshold:
|
| 339 |
+
return
|
| 340 |
+
|
| 341 |
+
to_summarize = count - keep_recent
|
| 342 |
+
if to_summarize < 5:
|
| 343 |
+
return
|
| 344 |
+
|
| 345 |
+
old_messages = await db.get_oldest_unsummarized(user_id, to_summarize)
|
| 346 |
+
if len(old_messages) < 5:
|
| 347 |
+
return
|
| 348 |
+
|
| 349 |
+
dialog_text = "\n".join([
|
| 350 |
+
f"{m['role']}: {m['content']}" for m in old_messages
|
| 351 |
+
])
|
| 352 |
+
|
| 353 |
+
summary = await glm_service.summarize(dialog_text)
|
| 354 |
+
if not summary:
|
| 355 |
+
return
|
| 356 |
+
|
| 357 |
+
cutoff_id = old_messages[-1]["id"]
|
| 358 |
+
await db.save_summary(user_id, summary, len(old_messages))
|
| 359 |
+
await db.mark_summarized(user_id, cutoff_id)
|
| 360 |
+
|
| 361 |
+
logger.info(
|
| 362 |
+
"Summarized %d messages for user %s, cutoff_id=%s",
|
| 363 |
+
len(old_messages), user_id, cutoff_id
|
| 364 |
+
)
|
| 365 |
+
except Exception as e:
|
| 366 |
+
logger.error("Summarization failed for user %s: %s", user_id, e, exc_info=True)
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
@router.message()
|
| 370 |
+
async def handle_chat(message: types.Message) -> None:
|
| 371 |
+
if not message.text:
|
| 372 |
+
await message.answer("❌ Поддерживаются только текстовые сообщения.")
|
| 373 |
+
return
|
| 374 |
+
|
| 375 |
+
user_id = message.from_user.id
|
| 376 |
+
user_text = message.text
|
| 377 |
+
|
| 378 |
+
# Save/update user
|
| 379 |
+
await db.upsert_user(
|
| 380 |
+
user_id=user_id,
|
| 381 |
+
username=message.from_user.username,
|
| 382 |
+
first_name=message.from_user.first_name,
|
| 383 |
+
last_name=message.from_user.last_name,
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
# Save user message
|
| 387 |
+
await db.save_message(user_id, "user", user_text)
|
| 388 |
+
|
| 389 |
+
# Get history (unsummarized only)
|
| 390 |
+
history = await db.get_messages(user_id, limit=config.MAX_HISTORY)
|
| 391 |
+
|
| 392 |
+
# Build messages for GLM
|
| 393 |
+
messages: list[dict] = [SYSTEM_PROMPT]
|
| 394 |
+
|
| 395 |
+
# Add summary as context if exists
|
| 396 |
+
summary = await db.get_summary(user_id)
|
| 397 |
+
if summary:
|
| 398 |
+
messages.append({
|
| 399 |
+
"role": "system",
|
| 400 |
+
"content": f"[Контекст предыдущих диалогов: {summary}]"
|
| 401 |
+
})
|
| 402 |
+
|
| 403 |
+
# Add recent conversation history
|
| 404 |
+
for h in history:
|
| 405 |
+
messages.append({"role": h["role"], "content": h["content"]})
|
| 406 |
+
|
| 407 |
+
# Add current user message (already saved in DB, but need it in the prompt)
|
| 408 |
+
if not history or history[-1]["content"] != user_text:
|
| 409 |
+
messages.append({"role": "user", "content": user_text})
|
| 410 |
+
|
| 411 |
+
# Send typing action
|
| 412 |
+
try:
|
| 413 |
+
await message.bot.send_chat_action(user_id, "typing")
|
| 414 |
+
except TelegramAPIError as e:
|
| 415 |
+
logger.warning("Failed to send typing action: %s", e)
|
| 416 |
+
|
| 417 |
+
try:
|
| 418 |
+
# Call GLM
|
| 419 |
+
response = await glm_service.chat(messages)
|
| 420 |
+
|
| 421 |
+
# Save assistant response
|
| 422 |
+
await db.save_message(user_id, "assistant", response)
|
| 423 |
+
|
| 424 |
+
# Trigger background summarization
|
| 425 |
+
asyncio.create_task(_maybe_summarize(user_id))
|
| 426 |
+
|
| 427 |
+
# Send response (handle long messages)
|
| 428 |
+
await send_long_message(message, response)
|
| 429 |
+
|
| 430 |
+
except Exception as e:
|
| 431 |
+
logger.error("Error in chat handler: %s", e, exc_info=True)
|
| 432 |
+
await message.answer(
|
| 433 |
+
"❌ Ошибка при обращении к модели. Попробуйте позже."
|
| 434 |
+
)
|
| 435 |
+
|
handlers commands.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import logging
|
| 3 |
+
from aiogram import Router, types
|
| 4 |
+
from aiogram.filters import Command
|
| 5 |
+
from database import db
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
router = Router()
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@router.message(Command("start"))
|
| 12 |
+
async def cmd_start(message: types.Message) -> None:
|
| 13 |
+
await message.answer(
|
| 14 |
+
"👋 Привет! Я AI-ассистент на базе GLM-5.1.\n\n"
|
| 15 |
+
"Отправь любое сообщение — я отвечу.\n"
|
| 16 |
+
"Используй /help для списка команд."
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@router.message(Command("help"))
|
| 21 |
+
async def cmd_help(message: types.Message) -> None:
|
| 22 |
+
await message.answer(
|
| 23 |
+
"📋 <b>Команды:</b>\n\n"
|
| 24 |
+
"/start — Начать работу\n"
|
| 25 |
+
"/help — Помощь\n"
|
| 26 |
+
"/clear — Очистить историю сообщений\n"
|
| 27 |
+
"/stats — Статистика\n"
|
| 28 |
+
"/ping — Проверка задержки"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
@router.message(Command("clear"))
|
| 33 |
+
async def cmd_clear(message: types.Message) -> None:
|
| 34 |
+
count = await db.clear_history(message.from_user.id)
|
| 35 |
+
await message.answer(f"🗑 История очищена. Удалено сообщений: {count}.")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@router.message(Command("stats"))
|
| 39 |
+
async def cmd_stats(message: types.Message) -> None:
|
| 40 |
+
stats = await db.get_stats(message.from_user.id)
|
| 41 |
+
await message.answer(
|
| 42 |
+
f"📊 <b>Статистика:</b>\n\n"
|
| 43 |
+
f"Всего пользователей: {stats['total_users']}\n"
|
| 44 |
+
f"Ваших сообщений: {stats['user_messages']}\n"
|
| 45 |
+
f"Всего сообщений в боте: {stats['total_messages']}"
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@router.message(Command("ping"))
|
| 50 |
+
async def cmd_ping(message: types.Message) -> None:
|
| 51 |
+
start = asyncio.get_event_loop().time()
|
| 52 |
+
msg = await message.answer("🏓 Pong!")
|
| 53 |
+
end = asyncio.get_event_loop().time()
|
| 54 |
+
latency = (end - start) * 1000
|
| 55 |
+
await msg.edit_text(f"🏓 Pong! Задержка: {latency:.1f} мс")
|
middlewares __init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
from .owner import OwnerMiddleware
|
middlewares owner.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Any, Awaitable, Callable, Dict
|
| 3 |
+
from aiogram import BaseMiddleware
|
| 4 |
+
from aiogram.types import Message, TelegramObject
|
| 5 |
+
from config import config
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class OwnerMiddleware(BaseMiddleware):
|
| 11 |
+
async def __call__(
|
| 12 |
+
self,
|
| 13 |
+
handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],
|
| 14 |
+
event: TelegramObject,
|
| 15 |
+
data: Dict[str, Any],
|
| 16 |
+
) -> Any:
|
| 17 |
+
if not isinstance(event, Message):
|
| 18 |
+
return await handler(event, data)
|
| 19 |
+
|
| 20 |
+
user_id = event.from_user.id
|
| 21 |
+
if user_id != config.OWNER_ID:
|
| 22 |
+
logger.warning("Unauthorized access from user_id=%s", user_id)
|
| 23 |
+
await event.answer("⛔ Доступ запрещён. Этот бот только для владельца.")
|
| 24 |
+
return None
|
| 25 |
+
|
| 26 |
+
return await handler(event, data)
|
service __init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .glm import glm_service
|
| 2 |
+
|
services glm.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import openai
|
| 2 |
+
import logging
|
| 3 |
+
from typing import List, Dict, Any
|
| 4 |
+
from config import config
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class GLMService:
|
| 10 |
+
def __init__(self) -> None:
|
| 11 |
+
self.client = openai.AsyncOpenAI(
|
| 12 |
+
base_url=config.NVIDIA_BASE_URL,
|
| 13 |
+
api_key=config.NVIDIA_API_KEY,
|
| 14 |
+
timeout=60.0,
|
| 15 |
+
max_retries=2,
|
| 16 |
+
)
|
| 17 |
+
self.model = config.MODEL_NAME
|
| 18 |
+
async def chat(self, messages: List[Dict[str, str]]) -> str:
|
| 19 |
+
try:
|
| 20 |
+
response = await self.client.chat.completions.create(
|
| 21 |
+
model=self.model,
|
| 22 |
+
messages=messages,
|
| 23 |
+
temperature=config.GLM_TEMPERATURE,
|
| 24 |
+
top_p=config.GLM_TOP_P,
|
| 25 |
+
frequency_penalty=config.GLM_FREQUENCY_PENALTY,
|
| 26 |
+
presence_penalty=config.GLM_PRESENCE_PENALTY,
|
| 27 |
+
max_tokens=config.GLM_MAX_TOKENS,
|
| 28 |
+
)
|
| 29 |
+
content = response.choices[0].message.content
|
| 30 |
+
if not content:
|
| 31 |
+
logger.warning("Empty response from GLM")
|
| 32 |
+
return "(пустой ответ от модели)"
|
| 33 |
+
logger.info("GLM response received, length: %d", len(content))
|
| 34 |
+
return content
|
| 35 |
+
except openai.APIError as e:
|
| 36 |
+
logger.error("GLM API error: %s", e)
|
| 37 |
+
raise
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error("Unexpected GLM error: %s", e)
|
| 40 |
+
raise
|
| 41 |
+
|
| 42 |
+
async def summarize(self, dialog_text: str) -> str:
|
| 43 |
+
"""Суммаризировать диалог через GLM."""
|
| 44 |
+
try:
|
| 45 |
+
messages = [
|
| 46 |
+
{
|
| 47 |
+
"role": "system",
|
| 48 |
+
"content": (
|
| 49 |
+
"Суммаризируй следующий диалог между пользователем и ассистентом. "
|
| 50 |
+
"Сохрани ключевые факты, предпочтения пользователя, важные детали и контекст. "
|
| 51 |
+
"Будь краток, максимум 4096 токенов. Используй русский язык."
|
| 52 |
+
)
|
| 53 |
+
},
|
| 54 |
+
{"role": "user", "content": dialog_text}
|
| 55 |
+
]
|
| 56 |
+
response = await self.client.chat.completions.create(
|
| 57 |
+
model=self.model,
|
| 58 |
+
messages=messages,
|
| 59 |
+
temperature=0.1,
|
| 60 |
+
max_tokens=config.SUMMARY_MAX_TOKENS,
|
| 61 |
+
)
|
| 62 |
+
content = response.choices[0].message.content
|
| 63 |
+
if not content:
|
| 64 |
+
logger.warning("Empty summary from GLM")
|
| 65 |
+
return ""
|
| 66 |
+
logger.info("Summary generated, length: %d", len(content))
|
| 67 |
+
return content
|
| 68 |
+
except Exception as e:
|
| 69 |
+
logger.error("Summary generation failed: %s", e)
|
| 70 |
+
return ""
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
glm_service = GLMService()
|
| 76 |
+
|
utils __init__.py
ADDED
|
File without changes
|
utils helpers.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from aiogram import types
|
| 3 |
+
from config import config
|
| 4 |
+
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
MAX_LENGTH = config.MAX_MESSAGE_LENGTH
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
async def send_long_message(message: types.Message, text: str) -> None:
|
| 10 |
+
"""Split long messages into chunks of MAX_LENGTH characters."""
|
| 11 |
+
if len(text) <= MAX_LENGTH:
|
| 12 |
+
await message.answer(text)
|
| 13 |
+
return
|
| 14 |
+
|
| 15 |
+
for i in range(0, len(text), MAX_LENGTH):
|
| 16 |
+
chunk = text[i:i + MAX_LENGTH]
|
| 17 |
+
await message.answer(chunk)
|