FreshPixels commited on
Commit
e683a50
·
verified ·
1 Parent(s): e859e98

Upload 12 files

Browse files
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)