Update app.py
Browse files
app.py
CHANGED
|
@@ -1,1631 +1,322 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
║ • Admin-only power • Group-safe • Self-evolving ║
|
| 10 |
-
║ • Image AI+PIL • Voice • Documents • Multi-model ║
|
| 11 |
-
║ • Supabase + SQLite + ChromaDB triple memory ║
|
| 12 |
-
║ • Bot spawner • Workflows • Broadcasts ║
|
| 13 |
-
╚══════════════════════════════════════════════════════════════════════════╝
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
import logging, asyncio, os, sys, json, subprocess, threading, time
|
| 17 |
-
import base64, io, re, hashlib, traceback
|
| 18 |
-
from datetime import datetime, timedelta
|
| 19 |
from urllib.parse import urlparse
|
| 20 |
-
from pathlib import Path
|
| 21 |
|
| 22 |
-
# ── Flask Bridge ──
|
| 23 |
from flask import Flask, request, Response
|
| 24 |
|
| 25 |
-
|
| 26 |
-
from aiogram import Bot, Dispatcher, types, F
|
| 27 |
from aiogram.filters import CommandStart, Command
|
| 28 |
-
from aiogram.
|
| 29 |
-
InlineKeyboardMarkup, InlineKeyboardButton,
|
| 30 |
-
BufferedInputFile, FSInputFile, CallbackQuery, Message,
|
| 31 |
-
InlineQuery, InlineQueryResultArticle, InputTextMessageContent,
|
| 32 |
-
BotCommand,
|
| 33 |
-
)
|
| 34 |
-
from aiogram.enums import ParseMode, ChatAction, ChatType
|
| 35 |
from aiogram.client.session.aiohttp import AiohttpSession
|
| 36 |
from aiogram.client.telegram import TelegramAPIServer
|
| 37 |
-
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
| 38 |
|
| 39 |
-
|
| 40 |
-
from agent import (
|
| 41 |
-
Config, Permission, DB, SupabaseDB, llm, memory, tools,
|
| 42 |
-
orchestrator, agent_team, doctor, rate_limiter,
|
| 43 |
-
ImageProcessor, AGENTS, ALL_TOOLS_SCHEMA,
|
| 44 |
-
)
|
| 45 |
|
| 46 |
-
logging.basicConfig(
|
| 47 |
-
|
| 48 |
-
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
| 49 |
-
handlers=[
|
| 50 |
-
logging.StreamHandler(),
|
| 51 |
-
logging.FileHandler(os.path.join(Config.LOGS_DIR, "bot.log"), encoding="utf-8"),
|
| 52 |
-
]
|
| 53 |
-
)
|
| 54 |
-
log = logging.getLogger("AgentForge")
|
| 55 |
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
#
|
| 60 |
-
# ║ HF blocks direct Python HTTP to api.telegram.org ║
|
| 61 |
-
# ║ But curl subprocess + --resolve + CF Worker = ✅ ║
|
| 62 |
-
# ║ ║
|
| 63 |
-
# ║ Flow: Bot → localhost:7860 → curl → CF Worker → Telegram API ║
|
| 64 |
-
# ╚═══════════════════════════════════════════════════════════════════╝
|
| 65 |
|
| 66 |
flask_app = Flask(__name__)
|
|
|
|
| 67 |
|
| 68 |
-
@flask_app.route(
|
| 69 |
def health():
|
| 70 |
-
uptime = time.time() - BOOT_TIME
|
| 71 |
-
h, m = divmod(int(uptime), 3600)
|
| 72 |
-
mins, s = divmod(m, 60)
|
| 73 |
return json.dumps({
|
| 74 |
-
"status": "
|
| 75 |
-
"
|
| 76 |
-
"
|
| 77 |
-
"
|
| 78 |
-
"models": len(llm.available_models()),
|
| 79 |
-
"spawned_bots": len(tools.spawned_bots),
|
| 80 |
}), 200, {"Content-Type": "application/json"}
|
| 81 |
|
| 82 |
-
|
| 83 |
-
@flask_app.route('/health')
|
| 84 |
-
def health_detail():
|
| 85 |
-
"""Detailed health endpoint for monitoring."""
|
| 86 |
-
try:
|
| 87 |
-
import psutil
|
| 88 |
-
return json.dumps({
|
| 89 |
-
"status": "ok",
|
| 90 |
-
"cpu": psutil.cpu_percent(),
|
| 91 |
-
"ram": psutil.virtual_memory().percent,
|
| 92 |
-
"disk": psutil.disk_usage('/').percent,
|
| 93 |
-
}), 200, {"Content-Type": "application/json"}
|
| 94 |
-
except:
|
| 95 |
-
return json.dumps({"status": "ok"}), 200
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def run_curl(method, url, data=None):
|
| 99 |
-
"""Execute curl with optional CF IP resolution to bypass blocks."""
|
| 100 |
parsed = urlparse(url)
|
| 101 |
domain = parsed.hostname
|
| 102 |
|
| 103 |
cmd = ["curl", "-X", method, url]
|
| 104 |
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
cmd += ["--resolve", f"{domain}:443:{Config.CLOUDFLARE_IP}"]
|
| 108 |
|
| 109 |
cmd += [
|
| 110 |
-
"-H", "User-Agent: Mozilla/5.0
|
| 111 |
"-H", "Content-Type: application/json",
|
| 112 |
-
"-k", "-s", "--max-time", "30"
|
| 113 |
]
|
| 114 |
|
| 115 |
-
|
| 116 |
-
if data:
|
| 117 |
-
cmd += ["--data-binary", "@-"]
|
| 118 |
-
|
| 119 |
|
| 120 |
try:
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
if not result.stdout:
|
| 126 |
-
log.warning(f"Curl empty response. stderr: {result.stderr[:200]}")
|
| 127 |
-
return json.dumps({"ok": False, "description": f"Empty curl response: {result.stderr[:200]}"})
|
| 128 |
-
return result.stdout
|
| 129 |
-
except subprocess.TimeoutExpired:
|
| 130 |
-
return json.dumps({"ok": False, "description": "Curl timeout (35s)"})
|
| 131 |
except Exception as e:
|
| 132 |
return json.dumps({"ok": False, "description": str(e)[:300]})
|
| 133 |
|
| 134 |
-
|
| 135 |
-
@flask_app.route('/bot<token>/<method>', methods=['POST', 'GET'])
|
| 136 |
def proxy(token, method):
|
| 137 |
-
|
| 138 |
-
real_url = f"{Config.PROXY_TARGET}/bot{token}/{method}"
|
| 139 |
-
|
| 140 |
-
# Force-parse JSON body
|
| 141 |
data = request.get_json(force=True, silent=True)
|
| 142 |
if not data:
|
| 143 |
data = request.form.to_dict() or request.args.to_dict() or None
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
return Response(resp, mimetype='application/json')
|
| 147 |
-
|
| 148 |
|
| 149 |
def start_bridge():
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
wlog.setLevel(logging.WARNING)
|
| 155 |
-
flask_app.run(host="0.0.0.0", port=Config.BRIDGE_PORT, threaded=True)
|
| 156 |
-
|
| 157 |
|
| 158 |
-
BOOT_TIME = time.time()
|
| 159 |
threading.Thread(target=start_bridge, daemon=True).start()
|
| 160 |
-
time.sleep(
|
| 161 |
-
log.info("🌉 Bridge ready")
|
| 162 |
-
|
| 163 |
|
| 164 |
-
#
|
| 165 |
-
#
|
| 166 |
-
#
|
| 167 |
|
| 168 |
-
session = AiohttpSession(
|
| 169 |
-
api=TelegramAPIServer.from_base(f"http://127.0.0.1:{Config.BRIDGE_PORT}")
|
| 170 |
-
)
|
| 171 |
bot = Bot(token=Config.BOT_TOKEN, session=session)
|
| 172 |
dp = Dispatcher()
|
| 173 |
-
scheduler = AsyncIOScheduler()
|
| 174 |
-
|
| 175 |
-
# Per-user state (non-persistent, resets on restart)
|
| 176 |
-
user_state: dict[int, dict] = {}
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
def get_state(uid: int) -> dict:
|
| 180 |
-
if uid not in user_state:
|
| 181 |
-
user_state[uid] = {
|
| 182 |
-
"agent": None,
|
| 183 |
-
"model": None,
|
| 184 |
-
"awaiting": None,
|
| 185 |
-
"last_message": "",
|
| 186 |
-
"last_response": None,
|
| 187 |
-
}
|
| 188 |
-
return user_state[uid]
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
def is_admin(uid: int) -> bool:
|
| 192 |
-
return Config.is_admin(uid)
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
def is_group(message: Message) -> bool:
|
| 196 |
-
return message.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP)
|
| 197 |
|
|
|
|
|
|
|
| 198 |
|
| 199 |
-
def should_respond_in_group(
|
| 200 |
-
|
| 201 |
-
if
|
| 202 |
return True
|
| 203 |
|
| 204 |
-
#
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
# @mention
|
| 210 |
-
bot_username = Config.BOT_USERNAME.lower()
|
| 211 |
-
if bot_username and message.text:
|
| 212 |
-
if f"@{bot_username}" in message.text.lower():
|
| 213 |
-
return True
|
| 214 |
|
| 215 |
-
#
|
| 216 |
-
if
|
| 217 |
-
for e in
|
| 218 |
if e.type == "mention":
|
| 219 |
-
mention =
|
| 220 |
-
if mention == f"@{
|
| 221 |
return True
|
| 222 |
|
| 223 |
return False
|
| 224 |
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
InlineKeyboardButton(text="⚙️ Settings", callback_data="nav:settings"),
|
| 240 |
-
InlineKeyboardButton(text="📊 Stats", callback_data="nav:stats")],
|
| 241 |
-
]
|
| 242 |
-
return InlineKeyboardMarkup(inline_keyboard=rows)
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
def kb_agents():
|
| 246 |
-
rows = []
|
| 247 |
-
for key, info in AGENTS.items():
|
| 248 |
-
if key == "director": continue # Director is always active
|
| 249 |
-
rows.append([InlineKeyboardButton(
|
| 250 |
-
text=f"{info['icon']} {info['name']}", callback_data=f"agent:{key}")])
|
| 251 |
-
rows.append([InlineKeyboardButton(text="🤖 Auto (Director decides)", callback_data="agent:auto")])
|
| 252 |
-
rows.append([InlineKeyboardButton(text="◀️ Back", callback_data="nav:main")])
|
| 253 |
-
return InlineKeyboardMarkup(inline_keyboard=rows)
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
def kb_models():
|
| 257 |
-
models = llm.available_models()
|
| 258 |
-
rows = []
|
| 259 |
-
row = []
|
| 260 |
-
for m in models:
|
| 261 |
-
short = m.split("/")[-1]
|
| 262 |
-
if len(short) > 20: short = short[:18] + ".."
|
| 263 |
-
row.append(InlineKeyboardButton(text=short, callback_data=f"model:{m}"))
|
| 264 |
-
if len(row) == 2:
|
| 265 |
-
rows.append(row); row = []
|
| 266 |
-
if row: rows.append(row)
|
| 267 |
-
rows.append([InlineKeyboardButton(text="◀️ Back", callback_data="nav:main")])
|
| 268 |
-
return InlineKeyboardMarkup(inline_keyboard=rows)
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
def kb_settings(uid):
|
| 272 |
-
user = DB.get_user(uid)
|
| 273 |
-
temp = user["temperature"] if user else 0.7
|
| 274 |
-
return InlineKeyboardMarkup(inline_keyboard=[
|
| 275 |
-
[InlineKeyboardButton(text=f"🌡️ Temperature: {temp}", callback_data="set:temp")],
|
| 276 |
-
[InlineKeyboardButton(text="📝 Custom System Prompt", callback_data="set:sysprompt")],
|
| 277 |
-
[InlineKeyboardButton(text="🎭 Personas", callback_data="set:personas")],
|
| 278 |
-
[InlineKeyboardButton(text="🔄 Reset Everything", callback_data="set:reset")],
|
| 279 |
-
[InlineKeyboardButton(text="◀️ Back", callback_data="nav:main")],
|
| 280 |
-
])
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
def kb_temp():
|
| 284 |
-
temps = [(0.0,"🧊 0.0"),(0.3,"😊 0.3"),(0.5,"🙂 0.5"),(0.7,"😄 0.7"),(0.9,"🔥 0.9"),(1.0,"🌋 1.0")]
|
| 285 |
-
rows = [[InlineKeyboardButton(text=l, callback_data=f"temp:{t}") for t, l in temps[i:i+3]]
|
| 286 |
-
for i in range(0, len(temps), 3)]
|
| 287 |
-
return InlineKeyboardMarkup(inline_keyboard=rows)
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
def kb_feedback():
|
| 291 |
-
return InlineKeyboardMarkup(inline_keyboard=[[
|
| 292 |
-
InlineKeyboardButton(text="👍", callback_data="fb:up"),
|
| 293 |
-
InlineKeyboardButton(text="👎", callback_data="fb:down"),
|
| 294 |
-
InlineKeyboardButton(text="🔄 Retry", callback_data="fb:retry"),
|
| 295 |
-
InlineKeyboardButton(text="🔊 Read", callback_data="fb:speak"),
|
| 296 |
-
]])
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
def kb_memory():
|
| 300 |
-
return InlineKeyboardMarkup(inline_keyboard=[
|
| 301 |
-
[InlineKeyboardButton(text="📝 View", callback_data="mem:view"),
|
| 302 |
-
InlineKeyboardButton(text="🔍 Search", callback_data="mem:search")],
|
| 303 |
-
[InlineKeyboardButton(text="🗑️ Clear All", callback_data="mem:clear"),
|
| 304 |
-
InlineKeyboardButton(text="◀️ Back", callback_data="nav:main")],
|
| 305 |
-
])
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
def kb_health():
|
| 309 |
-
return InlineKeyboardMarkup(inline_keyboard=[
|
| 310 |
-
[InlineKeyboardButton(text="🔄 Refresh", callback_data="health:refresh"),
|
| 311 |
-
InlineKeyboardButton(text="📋 Logs", callback_data="health:logs")],
|
| 312 |
-
])
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
def kb_admin():
|
| 316 |
-
return InlineKeyboardMarkup(inline_keyboard=[
|
| 317 |
-
[InlineKeyboardButton(text="📊 Full Stats", callback_data="adm:stats"),
|
| 318 |
-
InlineKeyboardButton(text="🏥 Health", callback_data="adm:health")],
|
| 319 |
-
[InlineKeyboardButton(text="📜 Tool Logs", callback_data="adm:toologs"),
|
| 320 |
-
InlineKeyboardButton(text="🤖 Bots", callback_data="adm:bots")],
|
| 321 |
-
[InlineKeyboardButton(text="🔄 Restart", callback_data="adm:restart"),
|
| 322 |
-
InlineKeyboardButton(text="💾 Backup DB", callback_data="adm:backup")],
|
| 323 |
-
])
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
# ╔═══════════════════════════════════════════════════════════════════╗
|
| 327 |
-
# ║ 💬 CORE RESPONSE SENDER (handles everything) ║
|
| 328 |
-
# ╚═══════════════════════════════════════════════════════════════════╝
|
| 329 |
-
|
| 330 |
-
async def send_response(message: Message, response: dict):
|
| 331 |
-
"""
|
| 332 |
-
Send the orchestrator's response. Handles:
|
| 333 |
-
- Long text splitting
|
| 334 |
-
- Image sending
|
| 335 |
-
- Audio/TTS sending
|
| 336 |
-
- Screenshot sending
|
| 337 |
-
- Tool usage footer
|
| 338 |
-
- Feedback buttons
|
| 339 |
-
- Broadcast / Restart triggers
|
| 340 |
-
"""
|
| 341 |
-
|
| 342 |
-
# ── Handle special actions ──
|
| 343 |
-
if response.get("_restart"):
|
| 344 |
-
await message.answer("🔄 Restarting in 3 seconds...")
|
| 345 |
-
await asyncio.sleep(3)
|
| 346 |
-
os.execv(sys.executable, [sys.executable] + sys.argv)
|
| 347 |
-
return
|
| 348 |
-
|
| 349 |
-
if response.get("_broadcast"):
|
| 350 |
-
msg_text = response["_broadcast"]
|
| 351 |
-
users = DB.execute("SELECT telegram_id FROM users WHERE is_banned=0", fetch=True)
|
| 352 |
-
sent, fail = 0, 0
|
| 353 |
-
for u in users:
|
| 354 |
-
try:
|
| 355 |
-
await bot.send_message(u["telegram_id"],
|
| 356 |
-
f"📢 <b>Announcement</b>\n\n{msg_text}", parse_mode=ParseMode.HTML)
|
| 357 |
-
sent += 1
|
| 358 |
-
except: fail += 1
|
| 359 |
-
await message.answer(f"📢 Broadcast complete: {sent} sent, {fail} failed")
|
| 360 |
-
# Also send the AI's text response
|
| 361 |
-
if response.get("text") and response["text"] != "Broadcast sent.":
|
| 362 |
-
pass # Fall through to send text below
|
| 363 |
-
else:
|
| 364 |
-
return
|
| 365 |
-
|
| 366 |
-
text = response.get("text", "")
|
| 367 |
-
images = response.get("images", [])
|
| 368 |
-
audio_files = response.get("audio_files", [])
|
| 369 |
-
screenshots = response.get("screenshots", [])
|
| 370 |
-
tools_used = response.get("tools_used", [])
|
| 371 |
-
tokens = response.get("tokens", 0)
|
| 372 |
-
model = response.get("model", "")
|
| 373 |
-
|
| 374 |
-
# ── Build footer ──
|
| 375 |
-
footer = ""
|
| 376 |
-
if tools_used:
|
| 377 |
-
unique = list(dict.fromkeys(tools_used))[:10]
|
| 378 |
-
footer += f"\n\n🛠️ <i>{', '.join(unique)}</i>"
|
| 379 |
-
footer += f"\n👑 <i>Director | {model.split('/')[-1]} | {tokens:,} tokens</i>"
|
| 380 |
-
|
| 381 |
-
full_text = text + footer
|
| 382 |
-
|
| 383 |
-
# ── Send text (with splitting) ──
|
| 384 |
-
MAX_LEN = 4000
|
| 385 |
-
if len(full_text) <= MAX_LEN:
|
| 386 |
-
await _safe_send(message, full_text, reply_markup=kb_feedback())
|
| 387 |
-
else:
|
| 388 |
-
# Split at newlines
|
| 389 |
-
chunks = _split_text(text, MAX_LEN - 200)
|
| 390 |
-
for i, chunk in enumerate(chunks):
|
| 391 |
-
is_last = (i == len(chunks) - 1)
|
| 392 |
-
prefix = f"📄 <b>Part {i+1}/{len(chunks)}</b>\n\n" if len(chunks) > 1 else ""
|
| 393 |
-
suffix = footer if is_last else ""
|
| 394 |
-
await _safe_send(message, prefix + chunk + suffix,
|
| 395 |
-
reply_markup=kb_feedback() if is_last else None)
|
| 396 |
-
if not is_last:
|
| 397 |
-
await asyncio.sleep(0.5) # Rate limit protection
|
| 398 |
-
|
| 399 |
-
# ── Send images ──
|
| 400 |
-
for img_url in images:
|
| 401 |
-
try:
|
| 402 |
-
import aiohttp
|
| 403 |
-
async with aiohttp.ClientSession() as s:
|
| 404 |
-
async with s.get(img_url, timeout=aiohttp.ClientTimeout(total=30)) as r:
|
| 405 |
-
img_bytes = await r.read()
|
| 406 |
-
await message.answer_photo(
|
| 407 |
-
photo=BufferedInputFile(img_bytes, filename="generated.png"),
|
| 408 |
-
caption="🎨 Generated Image")
|
| 409 |
-
except Exception as e:
|
| 410 |
-
await message.answer(f"🎨 Image URL: {img_url}")
|
| 411 |
-
|
| 412 |
-
# ── Send audio ──
|
| 413 |
-
for audio_path in audio_files:
|
| 414 |
-
try:
|
| 415 |
-
if os.path.exists(audio_path):
|
| 416 |
-
await message.answer_voice(
|
| 417 |
-
voice=FSInputFile(audio_path),
|
| 418 |
-
caption="🔊 Text-to-Speech")
|
| 419 |
-
# Cleanup
|
| 420 |
-
try: os.unlink(audio_path)
|
| 421 |
-
except: pass
|
| 422 |
-
except Exception as e:
|
| 423 |
-
log.warning(f"Audio send failed: {e}")
|
| 424 |
-
|
| 425 |
-
# ── Send screenshots ──
|
| 426 |
-
for ss_path in screenshots:
|
| 427 |
try:
|
| 428 |
-
|
| 429 |
-
await message.answer_photo(
|
| 430 |
-
photo=FSInputFile(ss_path),
|
| 431 |
-
caption="📸 Screenshot")
|
| 432 |
-
try: os.unlink(ss_path)
|
| 433 |
-
except: pass
|
| 434 |
-
except Exception as e:
|
| 435 |
-
log.warning(f"Screenshot send failed: {e}")
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
async def _safe_send(message: Message, text: str, **kwargs):
|
| 439 |
-
"""Send with HTML, fallback to plain text."""
|
| 440 |
-
if not text.strip():
|
| 441 |
-
text = "✅ Done."
|
| 442 |
-
try:
|
| 443 |
-
await message.answer(text, parse_mode=ParseMode.HTML, **kwargs)
|
| 444 |
-
except Exception:
|
| 445 |
-
try:
|
| 446 |
-
# Strip HTML tags for fallback
|
| 447 |
-
import re
|
| 448 |
-
clean = re.sub(r'<[^>]+>', '', text)
|
| 449 |
-
await message.answer(clean, **kwargs)
|
| 450 |
-
except Exception as e:
|
| 451 |
-
await message.answer(f"Response error: {str(e)[:200]}")
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
def _split_text(text: str, max_len: int) -> list[str]:
|
| 455 |
-
"""Split text at natural break points."""
|
| 456 |
-
if len(text) <= max_len:
|
| 457 |
-
return [text]
|
| 458 |
-
chunks = []
|
| 459 |
-
remaining = text
|
| 460 |
-
while remaining:
|
| 461 |
-
if len(remaining) <= max_len:
|
| 462 |
-
chunks.append(remaining)
|
| 463 |
-
break
|
| 464 |
-
# Try to split at double newline, then single newline, then space
|
| 465 |
-
split_at = -1
|
| 466 |
-
for sep in ['\n\n', '\n', '. ', ' ']:
|
| 467 |
-
idx = remaining.rfind(sep, 0, max_len)
|
| 468 |
-
if idx > max_len // 3: # Don't split too early
|
| 469 |
-
split_at = idx + len(sep)
|
| 470 |
-
break
|
| 471 |
-
if split_at == -1:
|
| 472 |
-
split_at = max_len
|
| 473 |
-
chunks.append(remaining[:split_at])
|
| 474 |
-
remaining = remaining[split_at:]
|
| 475 |
-
return chunks
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
# ╔═══════════════════════════════════════════════════════════════════╗
|
| 479 |
-
# ║ 🔄 PROCESS PIPELINE (middleware → agent → send) ║
|
| 480 |
-
# ╚═══════════════════════════════════════════════════════════════════╝
|
| 481 |
-
|
| 482 |
-
async def process_and_reply(message: Message, text: str,
|
| 483 |
-
attachments: list = None, force_admin: bool = False):
|
| 484 |
-
"""Full pipeline: auth → rate limit → orchestrator → send."""
|
| 485 |
-
uid = message.from_user.id
|
| 486 |
-
cid = message.chat.id
|
| 487 |
-
group = is_group(message)
|
| 488 |
-
|
| 489 |
-
# ── Middleware: Auth ──
|
| 490 |
-
user = DB.upsert_user(
|
| 491 |
-
uid,
|
| 492 |
-
message.from_user.username or "",
|
| 493 |
-
message.from_user.first_name or "",
|
| 494 |
-
message.from_user.last_name or "")
|
| 495 |
-
|
| 496 |
-
if user and user["is_banned"] and not is_admin(uid):
|
| 497 |
-
return await message.answer("🚫 You are banned from using this bot.")
|
| 498 |
-
|
| 499 |
-
# ── Middleware: Rate Limit ──
|
| 500 |
-
if rate_limiter.check(uid, is_admin(uid)):
|
| 501 |
-
return await message.answer(
|
| 502 |
-
"⏳ Rate limit reached. Please wait a moment.\n"
|
| 503 |
-
f"({'Unlimited' if is_admin(uid) else f'{Config.RATE_LIMIT_USER}/{Config.RATE_WINDOW}s'})")
|
| 504 |
-
|
| 505 |
-
# ── Non-admin in DM: limited response ──
|
| 506 |
-
if not is_admin(uid) and not group:
|
| 507 |
-
pass # Allow but tools are filtered by Permission system
|
| 508 |
-
|
| 509 |
-
# ── Status message ──
|
| 510 |
-
status = await message.answer("💭 Thinking...")
|
| 511 |
-
try:
|
| 512 |
-
await bot.send_chat_action(cid, ChatAction.TYPING)
|
| 513 |
-
except: pass
|
| 514 |
-
|
| 515 |
-
# ── User settings ──
|
| 516 |
-
state = get_state(uid)
|
| 517 |
-
state["last_message"] = text
|
| 518 |
-
|
| 519 |
-
settings = {
|
| 520 |
-
"preferred_model": state.get("model") or (user["preferred_model"] if user else Config.DEFAULT_MODEL),
|
| 521 |
-
"temperature": user["temperature"] if user else 0.7,
|
| 522 |
-
"system_prompt": user["system_prompt"] if user else "",
|
| 523 |
-
"max_tokens": user["max_tokens"] if user else 4096,
|
| 524 |
-
}
|
| 525 |
-
|
| 526 |
-
try:
|
| 527 |
-
# ── Run orchestrator ──
|
| 528 |
-
response = await orchestrator.process(
|
| 529 |
-
user_id=uid, chat_id=cid, message=text,
|
| 530 |
-
model=settings["preferred_model"],
|
| 531 |
-
attachments=attachments,
|
| 532 |
-
user_settings=settings,
|
| 533 |
-
is_group=group)
|
| 534 |
-
|
| 535 |
-
# ── Update stats ──
|
| 536 |
-
DB.inc_usage(uid, response.get("tokens", 0))
|
| 537 |
-
state["last_response"] = response
|
| 538 |
-
|
| 539 |
-
# ── Delete status ──
|
| 540 |
-
try: await status.delete()
|
| 541 |
-
except: pass
|
| 542 |
-
|
| 543 |
-
# ── Send response ──
|
| 544 |
-
await send_response(message, response)
|
| 545 |
-
|
| 546 |
-
except Exception as e:
|
| 547 |
-
log.error(f"Processing error: {traceback.format_exc()}")
|
| 548 |
-
try:
|
| 549 |
-
await status.edit_text(f"❌ Error: {str(e)[:300]}")
|
| 550 |
except:
|
| 551 |
-
await
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
# ╔═══════════════════════════════════════════════════════════════════╗
|
| 555 |
-
# ║ 📝 COMMAND HANDLERS ║
|
| 556 |
-
# ╚═══════════════════════════════════════════════════════════════════╝
|
| 557 |
-
|
| 558 |
-
@dp.message(CommandStart())
|
| 559 |
-
async def cmd_start(message: Message):
|
| 560 |
-
uid = message.from_user.id
|
| 561 |
-
DB.upsert_user(uid, message.from_user.username or "",
|
| 562 |
-
message.from_user.first_name or "", "")
|
| 563 |
-
name = message.from_user.first_name or "there"
|
| 564 |
-
is_adm = "👑 Admin" if is_admin(uid) else "👤 User"
|
| 565 |
-
|
| 566 |
-
# Dynamic buttons from Supabase
|
| 567 |
-
extra_buttons = await SupabaseDB.get_buttons()
|
| 568 |
-
extra_rows = []
|
| 569 |
-
row = []
|
| 570 |
-
for b in extra_buttons:
|
| 571 |
-
row.append(InlineKeyboardButton(text=b["label"], url=b["url"]))
|
| 572 |
-
if len(row) == 2:
|
| 573 |
-
extra_rows.append(row); row = []
|
| 574 |
-
if row: extra_rows.append(row)
|
| 575 |
-
|
| 576 |
-
kb = kb_main()
|
| 577 |
-
if extra_rows:
|
| 578 |
-
kb = InlineKeyboardMarkup(inline_keyboard=kb.inline_keyboard + extra_rows)
|
| 579 |
-
|
| 580 |
-
await message.answer(
|
| 581 |
-
f"""🚀 <b>Welcome to AgentForge, {name}!</b> ({is_adm})
|
| 582 |
-
|
| 583 |
-
I'm a <b>fully autonomous multi-agent AI system</b>.
|
| 584 |
-
|
| 585 |
-
👑 <b>Director</b> leads a team of <b>10 specialist agents</b>:
|
| 586 |
-
💻 Coder • 🔬 Researcher • ⚙️ SysAdmin
|
| 587 |
-
🎨 Creative • 📊 Analyst • 🏥 Doctor
|
| 588 |
-
📁 FileManager • 🛡️ Security
|
| 589 |
-
|
| 590 |
-
🛠️ <b>30+ Tools</b> — Search, Code, Shell, Files,
|
| 591 |
-
Self-Modify, Bot Spawn, Images, TTS, Memory...
|
| 592 |
-
|
| 593 |
-
🧠 <b>Triple Memory</b> — Remembers you forever
|
| 594 |
-
🔄 <b>Multi-Model</b> — GPT / Claude / Gemini / Llama / Custom
|
| 595 |
-
🏥 <b>Always-Alive</b> — Doctor monitors 24/7
|
| 596 |
-
|
| 597 |
-
Just send a message or use /help!""",
|
| 598 |
-
parse_mode=ParseMode.HTML, reply_markup=kb)
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
@dp.message(Command("help"))
|
| 602 |
-
async def cmd_help(message: Message):
|
| 603 |
-
admin_section = """
|
| 604 |
-
<b>👑 Admin:</b>
|
| 605 |
-
/admin - Admin panel
|
| 606 |
-
/broadcast <msg> - Send to all users
|
| 607 |
-
/ban <id> / /unban <id>
|
| 608 |
-
/premium <id> - Toggle premium
|
| 609 |
-
/shell <cmd> - Run shell command
|
| 610 |
-
/sysinfo - System details
|
| 611 |
-
/health - Health check
|
| 612 |
-
/logs - Recent tool logs
|
| 613 |
-
/restart - Restart bot
|
| 614 |
-
/addbutton <label>|<url> - Add /start button
|
| 615 |
-
""" if is_admin(message.from_user.id) else ""
|
| 616 |
-
|
| 617 |
-
await message.answer(f"""📚 <b>AgentForge Help</b>
|
| 618 |
-
|
| 619 |
-
<b>💬 Chat:</b>
|
| 620 |
-
/start - Welcome screen
|
| 621 |
-
/help - This menu
|
| 622 |
-
/clear - Clear conversation
|
| 623 |
-
/agent - Switch agent mode
|
| 624 |
-
/model - Switch AI model
|
| 625 |
-
/settings - Preferences
|
| 626 |
-
|
| 627 |
-
<b>🛠️ Actions:</b>
|
| 628 |
-
/search <query> - Web search
|
| 629 |
-
/code <code or task> - Code execution
|
| 630 |
-
/image <desc> - Generate image (DALL-E)
|
| 631 |
-
/summarize <text/url> - Summarize
|
| 632 |
-
/translate <lang> <text> - Translate
|
| 633 |
-
/tts <text> - Text to speech
|
| 634 |
-
|
| 635 |
-
<b>📁 Files:</b>
|
| 636 |
-
/read <path> - Read file
|
| 637 |
-
/write <path> <content> - Write file
|
| 638 |
-
/ls <path> - List files
|
| 639 |
-
|
| 640 |
-
<b>🧠 Memory:</b>
|
| 641 |
-
/memory - Memory panel
|
| 642 |
-
/remember <fact> - Store memory
|
| 643 |
-
/recall <query> - Search memory
|
| 644 |
-
|
| 645 |
-
<b>🤖 Advanced:</b>
|
| 646 |
-
/newbot <token> - Spawn new bot
|
| 647 |
-
/bots - List spawned bots
|
| 648 |
-
/self <instruction> - Self-modify
|
| 649 |
-
/workflow - Workflows
|
| 650 |
-
/persona <name> <prompt> - Save persona
|
| 651 |
-
/personas - List personas
|
| 652 |
-
/tools - All available tools
|
| 653 |
-
|
| 654 |
-
<b>📊 Info:</b>
|
| 655 |
-
/stats - Your statistics
|
| 656 |
-
/export - Export conversation
|
| 657 |
-
/feedback <msg> - Feedback
|
| 658 |
-
{admin_section}
|
| 659 |
-
<b>💡 Also handles:</b> 📷 Photos, 🎙️ Voice, 📄 Documents, 😀 Stickers
|
| 660 |
-
<b>💡 Groups:</b> Reply to my message or @mention me""",
|
| 661 |
-
parse_mode=ParseMode.HTML)
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
@dp.message(Command("agent"))
|
| 665 |
-
async def cmd_agent(message: Message):
|
| 666 |
-
state = get_state(message.from_user.id)
|
| 667 |
-
current = state.get("agent") or "auto (Director decides)"
|
| 668 |
-
await message.answer(
|
| 669 |
-
f"🤖 <b>Agent Mode</b>\nCurrent: <b>{current}</b>\n\nDirector always coordinates — choose a specialist focus:",
|
| 670 |
-
parse_mode=ParseMode.HTML, reply_markup=kb_agents())
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
@dp.message(Command("model"))
|
| 674 |
-
async def cmd_model(message: Message):
|
| 675 |
-
state = get_state(message.from_user.id)
|
| 676 |
-
current = state.get("model") or Config.DEFAULT_MODEL
|
| 677 |
-
await message.answer(f"🔄 <b>AI Model</b>\nCurrent: <code>{current}</code>",
|
| 678 |
-
parse_mode=ParseMode.HTML, reply_markup=kb_models())
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
@dp.message(Command("clear"))
|
| 682 |
-
async def cmd_clear(message: Message):
|
| 683 |
-
uid = message.from_user.id
|
| 684 |
-
memory.clear(uid, message.chat.id)
|
| 685 |
-
get_state(uid).update({"agent": None, "last_message": "", "last_response": None})
|
| 686 |
-
await message.answer("🗑️ Conversation cleared! Starting fresh.")
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
@dp.message(Command("settings"))
|
| 690 |
-
async def cmd_settings(message: Message):
|
| 691 |
-
uid = message.from_user.id
|
| 692 |
-
user = DB.get_user(uid)
|
| 693 |
-
state = get_state(uid)
|
| 694 |
-
m = state.get("model") or (user["preferred_model"] if user else Config.DEFAULT_MODEL)
|
| 695 |
-
await message.answer(
|
| 696 |
-
f"⚙️ <b>Settings</b>\n\n"
|
| 697 |
-
f"🔄 Model: <code>{m}</code>\n"
|
| 698 |
-
f"🌡️ Temp: <code>{user['temperature'] if user else 0.7}</code>\n"
|
| 699 |
-
f"📝 Prompt: <code>{(user['system_prompt'][:40]+'...') if user and user['system_prompt'] else 'Default'}</code>",
|
| 700 |
-
parse_mode=ParseMode.HTML, reply_markup=kb_settings(uid))
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
@dp.message(Command("search"))
|
| 704 |
-
async def cmd_search(message: Message):
|
| 705 |
-
q = message.text.split(maxsplit=1)
|
| 706 |
-
if len(q) < 2: return await message.answer("Usage: /search <query>", parse_mode=ParseMode.HTML)
|
| 707 |
-
await process_and_reply(message, f"Research this thoroughly with multiple searches: {q[1]}")
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
@dp.message(Command("code"))
|
| 711 |
-
async def cmd_code(message: Message):
|
| 712 |
-
c = message.text.split(maxsplit=1)
|
| 713 |
-
if len(c) < 2: return await message.answer("Usage: /code <code or description>", parse_mode=ParseMode.HTML)
|
| 714 |
-
if not is_admin(message.from_user.id):
|
| 715 |
-
return await message.answer("🚫 Code execution is admin-only.")
|
| 716 |
-
await process_and_reply(message, c[1])
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
@dp.message(Command("image"))
|
| 720 |
-
async def cmd_image(message: Message):
|
| 721 |
-
p = message.text.split(maxsplit=1)
|
| 722 |
-
if len(p) < 2: return await message.answer("Usage: /image <description>", parse_mode=ParseMode.HTML)
|
| 723 |
-
await process_and_reply(message, f"Generate an image of: {p[1]}")
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
@dp.message(Command("summarize"))
|
| 727 |
-
async def cmd_summarize(message: Message):
|
| 728 |
-
text = message.text.split(maxsplit=1)
|
| 729 |
-
if len(text) < 2:
|
| 730 |
-
if message.reply_to_message and message.reply_to_message.text:
|
| 731 |
-
text = ["", message.reply_to_message.text]
|
| 732 |
-
else:
|
| 733 |
-
return await message.answer("Usage: /summarize <text or URL>", parse_mode=ParseMode.HTML)
|
| 734 |
-
await process_and_reply(message, f"Summarize this concisely:\n\n{text[1]}")
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
@dp.message(Command("translate"))
|
| 738 |
-
async def cmd_translate(message: Message):
|
| 739 |
-
parts = message.text.split(maxsplit=2)
|
| 740 |
-
if len(parts) < 3:
|
| 741 |
-
return await message.answer("Usage: /translate <language> <text>", parse_mode=ParseMode.HTML)
|
| 742 |
-
await process_and_reply(message, f"Translate to {parts[1]}:\n\n{parts[2]}")
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
@dp.message(Command("tts"))
|
| 746 |
-
async def cmd_tts(message: Message):
|
| 747 |
-
t = message.text.split(maxsplit=1)
|
| 748 |
-
if len(t) < 2: return await message.answer("Usage: /tts <text>", parse_mode=ParseMode.HTML)
|
| 749 |
-
status = await message.answer("🔊 Generating speech...")
|
| 750 |
-
try:
|
| 751 |
-
audio = await llm.tts(t[1])
|
| 752 |
-
if audio:
|
| 753 |
-
path = os.path.join(Config.DATA_DIR, f"tts_{int(time.time())}.mp3")
|
| 754 |
-
with open(path, "wb") as f: f.write(audio)
|
| 755 |
-
await message.answer_voice(voice=FSInputFile(path))
|
| 756 |
-
try: os.unlink(path)
|
| 757 |
-
except: pass
|
| 758 |
-
else:
|
| 759 |
-
await message.answer("❌ TTS unavailable (needs OpenAI key)")
|
| 760 |
-
except Exception as e:
|
| 761 |
-
await message.answer(f"❌ TTS error: {str(e)[:200]}")
|
| 762 |
-
try: await status.delete()
|
| 763 |
-
except: pass
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
@dp.message(Command("read"))
|
| 767 |
-
async def cmd_read(message: Message):
|
| 768 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 769 |
-
p = message.text.split(maxsplit=1)
|
| 770 |
-
if len(p) < 2: return await message.answer("Usage: /read <path>", parse_mode=ParseMode.HTML)
|
| 771 |
-
r = await tools.execute("file_read", {"path": p[1]}, message.from_user.id)
|
| 772 |
-
await _safe_send(message, r[:4000])
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
@dp.message(Command("write"))
|
| 776 |
-
async def cmd_write(message: Message):
|
| 777 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 778 |
-
parts = message.text.split(maxsplit=2)
|
| 779 |
-
if len(parts) < 3: return await message.answer("Usage: /write <path> <content>", parse_mode=ParseMode.HTML)
|
| 780 |
-
r = await tools.execute("file_write", {"path": parts[1], "content": parts[2]}, message.from_user.id)
|
| 781 |
-
await message.answer(r)
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
@dp.message(Command("ls"))
|
| 785 |
-
async def cmd_ls(message: Message):
|
| 786 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 787 |
-
p = message.text.split(maxsplit=1)
|
| 788 |
-
path = p[1] if len(p) > 1 else "."
|
| 789 |
-
r = await tools.execute("file_list", {"path": path}, message.from_user.id)
|
| 790 |
-
await _safe_send(message, r[:4000])
|
| 791 |
|
|
|
|
|
|
|
|
|
|
| 792 |
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
await message.answer("🧠 <b>Memory Management</b>", parse_mode=ParseMode.HTML, reply_markup=kb_memory())
|
| 796 |
|
|
|
|
|
|
|
|
|
|
| 797 |
|
| 798 |
-
@dp.message(
|
| 799 |
-
async def
|
| 800 |
-
|
| 801 |
-
if len(f) < 2: return await message.answer("Usage: /remember <fact>", parse_mode=ParseMode.HTML)
|
| 802 |
-
await memory.store_long_term(message.from_user.id, f[1])
|
| 803 |
-
await message.answer(f"🧠 Stored: <i>{f[1][:100]}</i>", parse_mode=ParseMode.HTML)
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
@dp.message(Command("recall"))
|
| 807 |
-
async def cmd_recall(message: Message):
|
| 808 |
-
q = message.text.split(maxsplit=1)
|
| 809 |
-
if len(q) < 2: return await message.answer("Usage: /recall <query>", parse_mode=ParseMode.HTML)
|
| 810 |
-
r = await memory.recall(message.from_user.id, q[1], 7)
|
| 811 |
-
if not r: return await message.answer("🧠 Nothing found.")
|
| 812 |
-
await _safe_send(message, "🧠 <b>Recalled:</b>\n\n" + "\n".join(f"• {x}" for x in r))
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
@dp.message(Command("stats"))
|
| 816 |
-
async def cmd_stats(message: Message):
|
| 817 |
-
uid = message.from_user.id
|
| 818 |
-
user = DB.get_user(uid)
|
| 819 |
-
if not user: return await message.answer("No stats yet. Send a message first!")
|
| 820 |
await message.answer(
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
@dp.message(Command("export"))
|
| 833 |
-
async def cmd_export(message: Message):
|
| 834 |
-
uid = message.from_user.id
|
| 835 |
-
hist = memory.get_full(uid, message.chat.id)
|
| 836 |
-
if not hist: return await message.answer("📤 Nothing to export.")
|
| 837 |
-
text = "# AgentForge Conversation Export\n\n"
|
| 838 |
-
for m in hist:
|
| 839 |
-
ts = datetime.fromtimestamp(m.get("ts",0)).strftime("%H:%M:%S") if m.get("ts") else ""
|
| 840 |
-
agent = m.get("agent", "")
|
| 841 |
-
text += f"## {m['role'].title()} [{ts}] {f'({agent})' if agent else ''}\n{m['content']}\n\n---\n\n"
|
| 842 |
-
await message.answer_document(
|
| 843 |
-
BufferedInputFile(text.encode(), filename=f"export_{uid}_{int(time.time())}.md"),
|
| 844 |
-
caption="📤 Conversation Export")
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
@dp.message(Command("feedback"))
|
| 848 |
-
async def cmd_feedback(message: Message):
|
| 849 |
-
t = message.text.split(maxsplit=1)
|
| 850 |
-
if len(t) < 2: return await message.answer("Usage: /feedback <message>", parse_mode=ParseMode.HTML)
|
| 851 |
-
DB.execute("INSERT INTO feedback (user_id,comment) VALUES (?,?)", (message.from_user.id, t[1]))
|
| 852 |
-
await message.answer("💡 Thanks for your feedback!")
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
@dp.message(Command("newbot"))
|
| 856 |
-
async def cmd_newbot(message: Message):
|
| 857 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 858 |
-
parts = message.text.split(maxsplit=1)
|
| 859 |
-
if len(parts) < 2:
|
| 860 |
-
return await message.answer(
|
| 861 |
-
"Usage: /newbot <TOKEN>\n"
|
| 862 |
-
"Or: /newbot TOKEN|System prompt for the bot", parse_mode=ParseMode.HTML)
|
| 863 |
-
data = parts[1]
|
| 864 |
-
token, sp = (data.split("|",1) + ["You are a helpful assistant."])[:2]
|
| 865 |
-
status = await message.answer("🤖 Spawning bot...")
|
| 866 |
-
r = await tools.execute("spawn_bot",
|
| 867 |
-
{"token": token.strip(), "system_prompt": sp.strip(),
|
| 868 |
-
"name": f"Sub_{len(tools.spawned_bots)+1}"},
|
| 869 |
-
uid=message.from_user.id)
|
| 870 |
-
await _safe_send(message, r)
|
| 871 |
-
try: await status.delete()
|
| 872 |
-
except: pass
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
@dp.message(Command("bots"))
|
| 876 |
-
async def cmd_bots(message: Message):
|
| 877 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 878 |
-
r = await tools.execute("manage_bots", {"action": "list"}, message.from_user.id)
|
| 879 |
-
await _safe_send(message, r)
|
| 880 |
-
|
| 881 |
-
|
| 882 |
-
@dp.message(Command("self"))
|
| 883 |
-
async def cmd_self(message: Message):
|
| 884 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 885 |
-
instr = message.text.split(maxsplit=1)
|
| 886 |
-
if len(instr) < 2:
|
| 887 |
-
return await message.answer(
|
| 888 |
-
"Usage: /self <instruction>\n\nExamples:\n"
|
| 889 |
-
"• /self add a /joke command\n"
|
| 890 |
-
"• /self show your app.py source code\n"
|
| 891 |
-
"• /self improve error handling", parse_mode=ParseMode.HTML)
|
| 892 |
-
await process_and_reply(message,
|
| 893 |
-
f"SELF-MODIFICATION REQUEST: {instr[1]}\n\n"
|
| 894 |
-
"Read the relevant source files first (app.py, agent.py), "
|
| 895 |
-
"plan the changes, then apply them using self_modify tool. "
|
| 896 |
-
"Be careful and precise.")
|
| 897 |
-
|
| 898 |
|
| 899 |
@dp.message(Command("tools"))
|
| 900 |
-
async def
|
| 901 |
-
|
| 902 |
-
|
| 903 |
-
|
| 904 |
-
"📁 Files": ["file_read","file_write","file_delete","file_list"],
|
| 905 |
-
"🧬 Modify": ["self_modify"],
|
| 906 |
-
"🎨 Media": ["generate_image","analyze_image","text_to_speech","screenshot"],
|
| 907 |
-
"🧮 Math": ["calculator"],
|
| 908 |
-
"🌤️ Info": ["get_weather","system_info"],
|
| 909 |
-
"🧠 Memory": ["memory_store","memory_recall"],
|
| 910 |
-
"🌐 Net": ["http_request","translate_text","summarize_text"],
|
| 911 |
-
"🤖 Bots": ["spawn_bot","manage_bots"],
|
| 912 |
-
"📋 Auto": ["create_workflow","run_workflow"],
|
| 913 |
-
"👑 Admin": ["broadcast_message","ban_user","restart_system","send_email"],
|
| 914 |
-
"🤝 Team": ["delegate_task","agent_dispatch"],
|
| 915 |
-
}
|
| 916 |
-
text = f"🛠️ <b>{len(ALL_TOOLS_SCHEMA)} Tools Available</b>\n"
|
| 917 |
-
for cat, tnames in cats.items():
|
| 918 |
-
text += f"\n{cat}\n"
|
| 919 |
-
for tn in tnames: text += f" • <code>{tn}</code>\n"
|
| 920 |
-
await message.answer(text, parse_mode=ParseMode.HTML)
|
| 921 |
-
|
| 922 |
|
| 923 |
-
@dp.message(Command("
|
| 924 |
-
async def
|
| 925 |
-
if not is_admin(message.from_user.id):
|
| 926 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 927 |
if len(parts) < 2:
|
| 928 |
-
|
| 929 |
-
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
|
| 935 |
-
wid = int(parts[1])
|
| 936 |
-
status = await message.answer("▶️ Running workflow...")
|
| 937 |
-
r = await orchestrator.run_workflow(message.from_user.id, wid) if hasattr(orchestrator, 'run_workflow') else "Not supported"
|
| 938 |
-
await _safe_send(message, f"📋 <b>Result:</b>\n\n{r[:3500]}")
|
| 939 |
-
try: await status.delete()
|
| 940 |
-
except: pass
|
| 941 |
-
|
| 942 |
|
| 943 |
-
@dp.message(Command("
|
| 944 |
-
async def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 945 |
parts = message.text.split(maxsplit=2)
|
| 946 |
if len(parts) < 3:
|
| 947 |
-
return await message.answer("Usage: /
|
| 948 |
-
DB.execute("INSERT INTO personas (user_id,name,prompt) VALUES (?,?,?)",
|
| 949 |
-
(message.from_user.id, parts[1], parts[2]))
|
| 950 |
-
await message.answer(f"🎭 Persona '<b>{parts[1]}</b>' saved!", parse_mode=ParseMode.HTML)
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
@dp.message(Command("personas"))
|
| 954 |
-
async def cmd_personas(message: Message):
|
| 955 |
-
rows = DB.execute("SELECT * FROM personas WHERE user_id=?", (message.from_user.id,), fetch=True)
|
| 956 |
-
if not rows: return await message.answer("No personas. Use /persona <name> <prompt>", parse_mode=ParseMode.HTML)
|
| 957 |
-
text = "🎭 <b>Personas:</b>\n\n"
|
| 958 |
-
kb_rows = []
|
| 959 |
-
for r in rows:
|
| 960 |
-
text += f"• <b>{r['name']}</b>: {r['prompt'][:50]}...\n"
|
| 961 |
-
kb_rows.append([InlineKeyboardButton(text=f"🎭 {r['name']}", callback_data=f"persona:{r['id']}")])
|
| 962 |
-
kb_rows.append([InlineKeyboardButton(text="🗑️ Clear All", callback_data="persona:clear")])
|
| 963 |
-
await message.answer(text, parse_mode=ParseMode.HTML,
|
| 964 |
-
reply_markup=InlineKeyboardMarkup(inline_keyboard=kb_rows))
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
# ╔═══════════════════════════════════════════════════════════════════╗
|
| 968 |
-
# ║ 👑 ADMIN COMMANDS ║
|
| 969 |
-
# ╚═══════════════════════════════════════════════════════════════════╝
|
| 970 |
-
|
| 971 |
-
@dp.message(Command("admin"))
|
| 972 |
-
async def cmd_admin(message: Message):
|
| 973 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 974 |
-
stats = DB.user_stats()
|
| 975 |
-
tc = DB.execute("SELECT COUNT(*) as c FROM tool_usage", fetchone=True)["c"]
|
| 976 |
-
await message.answer(
|
| 977 |
-
f"👑 <b>Admin Panel</b>\n\n"
|
| 978 |
-
f"👥 Users: {stats['total']} ({stats['active_today']} today)\n"
|
| 979 |
-
f"💬 Messages: {stats['total_messages']:,}\n"
|
| 980 |
-
f"🔤 Tokens: {stats['total_tokens']:,}\n"
|
| 981 |
-
f"🛠️ Tool calls: {tc}\n"
|
| 982 |
-
f"🤖 Bots: {len(tools.spawned_bots)}\n"
|
| 983 |
-
f"🏥 Doctor: {'🟢 Active' if doctor.running else '🔴 Stopped'}\n"
|
| 984 |
-
f"⏱️ Uptime: {timedelta(seconds=int(time.time()-BOOT_TIME))}",
|
| 985 |
-
parse_mode=ParseMode.HTML, reply_markup=kb_admin())
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
@dp.message(Command("broadcast"))
|
| 989 |
-
async def cmd_broadcast(message: Message):
|
| 990 |
-
if not is_admin(message.from_user.id): return
|
| 991 |
-
t = message.text.split(maxsplit=1)
|
| 992 |
-
if len(t) < 2: return await message.answer("Usage: /broadcast <message>", parse_mode=ParseMode.HTML)
|
| 993 |
-
users = DB.execute("SELECT telegram_id FROM users WHERE is_banned=0", fetch=True)
|
| 994 |
-
sent, fail = 0, 0
|
| 995 |
-
for u in users:
|
| 996 |
-
try:
|
| 997 |
-
await bot.send_message(u["telegram_id"], f"📢 <b>Announcement</b>\n\n{t[1]}", parse_mode=ParseMode.HTML)
|
| 998 |
-
sent += 1
|
| 999 |
-
except: fail += 1
|
| 1000 |
-
await asyncio.sleep(0.1) # Rate limit
|
| 1001 |
-
await message.answer(f"📢 Done: {sent} sent, {fail} failed")
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
@dp.message(Command("ban"))
|
| 1005 |
-
async def cmd_ban(message: Message):
|
| 1006 |
-
if not is_admin(message.from_user.id): return
|
| 1007 |
try:
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
await message.answer(
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
try:
|
| 1018 |
-
uid = int(message.text.split()[1])
|
| 1019 |
-
DB.update_user(uid, is_banned=0)
|
| 1020 |
-
await message.answer(f"✅ User {uid} unbanned.")
|
| 1021 |
-
except: await message.answer("Usage: /unban <user_id>", parse_mode=ParseMode.HTML)
|
| 1022 |
-
|
| 1023 |
-
|
| 1024 |
-
@dp.message(Command("premium"))
|
| 1025 |
-
async def cmd_premium(message: Message):
|
| 1026 |
-
if not is_admin(message.from_user.id): return
|
| 1027 |
-
try:
|
| 1028 |
-
uid = int(message.text.split()[1])
|
| 1029 |
-
user = DB.get_user(uid)
|
| 1030 |
-
new = 0 if user and user["is_premium"] else 1
|
| 1031 |
-
DB.update_user(uid, is_premium=new)
|
| 1032 |
-
await message.answer(f"⭐ User {uid} premium: {'ON' if new else 'OFF'}")
|
| 1033 |
-
except: await message.answer("Usage: /premium <user_id>", parse_mode=ParseMode.HTML)
|
| 1034 |
-
|
| 1035 |
-
|
| 1036 |
-
@dp.message(Command("shell"))
|
| 1037 |
-
async def cmd_shell(message: Message):
|
| 1038 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 1039 |
-
c = message.text.split(maxsplit=1)
|
| 1040 |
-
if len(c) < 2: return await message.answer("Usage: /shell <command>", parse_mode=ParseMode.HTML)
|
| 1041 |
-
r = await tools.execute("run_shell", {"command": c[1]}, message.from_user.id)
|
| 1042 |
-
await _safe_send(message, r[:4000])
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
@dp.message(Command("sysinfo"))
|
| 1046 |
-
async def cmd_sysinfo(message: Message):
|
| 1047 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 1048 |
-
r = await tools.execute("system_info", {}, message.from_user.id)
|
| 1049 |
-
await _safe_send(message, r)
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
@dp.message(Command("health"))
|
| 1053 |
-
async def cmd_health(message: Message):
|
| 1054 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 1055 |
-
status = await message.answer("🏥 Running health check...")
|
| 1056 |
-
h = await doctor.check_health()
|
| 1057 |
-
report = doctor.format_report(h)
|
| 1058 |
-
try: await status.delete()
|
| 1059 |
-
except: pass
|
| 1060 |
-
await message.answer(report, reply_markup=kb_health())
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
@dp.message(Command("logs"))
|
| 1064 |
-
async def cmd_logs(message: Message):
|
| 1065 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 1066 |
-
rows = DB.execute("SELECT * FROM tool_usage ORDER BY created_at DESC LIMIT 25", fetch=True)
|
| 1067 |
-
text = "📜 <b>Recent Tool Usage:</b>\n\n"
|
| 1068 |
-
for r in rows:
|
| 1069 |
-
s = "✅" if r["success"] else "❌"
|
| 1070 |
-
text += f"{s} <code>{r['tool_name']}</code> by {r['agent_name']} | u:{r['user_id']} | {r['execution_time']:.1f}s\n"
|
| 1071 |
-
await _safe_send(message, text[:4000])
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
@dp.message(Command("restart"))
|
| 1075 |
-
async def cmd_restart(message: Message):
|
| 1076 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 1077 |
-
await message.answer("🔄 Restarting in 3 seconds...")
|
| 1078 |
-
await asyncio.sleep(3)
|
| 1079 |
-
os.execv(sys.executable, [sys.executable] + sys.argv)
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
@dp.message(Command("addbutton"))
|
| 1083 |
-
async def cmd_addbutton(message: Message):
|
| 1084 |
-
if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
|
| 1085 |
-
t = message.text.split(maxsplit=1)
|
| 1086 |
-
if len(t) < 2 or "|" not in t[1]:
|
| 1087 |
-
return await message.answer("Usage: /addbutton Label|https://url", parse_mode=ParseMode.HTML)
|
| 1088 |
-
label, url = t[1].split("|", 1)
|
| 1089 |
-
ok = await SupabaseDB.add_button(label.strip(), url.strip())
|
| 1090 |
-
DB.execute("INSERT INTO bot_buttons (label,url) VALUES (?,?)", (label.strip(), url.strip()))
|
| 1091 |
-
await message.answer(f"{'✅' if ok else '⚠️'} Button '{label.strip()}' added!")
|
| 1092 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1093 |
|
| 1094 |
-
#
|
| 1095 |
-
#
|
| 1096 |
-
#
|
| 1097 |
|
| 1098 |
@dp.message(F.text & ~F.text.startswith("/"))
|
| 1099 |
-
async def
|
| 1100 |
-
"""Handle all plain text messages."""
|
| 1101 |
-
# Group check
|
| 1102 |
if is_group(message) and not should_respond_in_group(message):
|
| 1103 |
return
|
| 1104 |
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
# Clean @mention from text in groups
|
| 1108 |
-
if Config.BOT_USERNAME:
|
| 1109 |
-
text = text.replace(f"@{Config.BOT_USERNAME}", "").strip()
|
| 1110 |
-
text = text.replace(f"@{Config.BOT_USERNAME.lower()}", "").strip()
|
| 1111 |
-
|
| 1112 |
-
# Reply context
|
| 1113 |
-
if message.reply_to_message and message.reply_to_message.text:
|
| 1114 |
-
reply_text = message.reply_to_message.text[:500]
|
| 1115 |
-
text = f"[Replying to: {reply_text}]\n\n{text}"
|
| 1116 |
-
|
| 1117 |
-
# Check awaiting state
|
| 1118 |
-
state = get_state(message.from_user.id)
|
| 1119 |
-
if state.get("awaiting") == "sysprompt":
|
| 1120 |
-
DB.update_user(message.from_user.id, system_prompt=text)
|
| 1121 |
-
state["awaiting"] = None
|
| 1122 |
-
return await message.answer(f"📝 System prompt set!\n<i>{text[:200]}</i>", parse_mode=ParseMode.HTML)
|
| 1123 |
-
|
| 1124 |
-
if state.get("awaiting") == "mem_search":
|
| 1125 |
-
state["awaiting"] = None
|
| 1126 |
-
r = await memory.recall(message.from_user.id, text, 7)
|
| 1127 |
-
if not r: return await message.answer("🧠 Nothing found.")
|
| 1128 |
-
return await _safe_send(message, "🧠 <b>Results:</b>\n\n" + "\n".join(f"• {x}" for x in r))
|
| 1129 |
-
|
| 1130 |
-
await process_and_reply(message, text)
|
| 1131 |
|
|
|
|
| 1132 |
|
| 1133 |
-
|
| 1134 |
-
async def handle_voice(message: Message):
|
| 1135 |
-
"""Voice → transcribe → process."""
|
| 1136 |
-
if is_group(message) and not should_respond_in_group(message):
|
| 1137 |
-
return
|
| 1138 |
-
|
| 1139 |
-
status = await message.answer("🎙️ Transcribing...")
|
| 1140 |
try:
|
| 1141 |
-
|
| 1142 |
-
|
| 1143 |
-
|
| 1144 |
-
audio_bytes = fdata.read() if hasattr(fdata, 'read') else bytes(fdata)
|
| 1145 |
-
|
| 1146 |
-
transcript = await llm.stt(audio_bytes)
|
| 1147 |
-
if not transcript:
|
| 1148 |
-
return await status.edit_text("❌ Could not transcribe. Need OpenAI key.")
|
| 1149 |
-
|
| 1150 |
-
await status.edit_text(f"🎙️ <i>{transcript[:200]}</i>\n\n💭 Processing...",
|
| 1151 |
-
parse_mode=ParseMode.HTML)
|
| 1152 |
-
try: await status.delete()
|
| 1153 |
-
except: pass
|
| 1154 |
-
|
| 1155 |
-
await process_and_reply(message, transcript)
|
| 1156 |
-
|
| 1157 |
-
except Exception as e:
|
| 1158 |
-
log.error(f"Voice error: {e}")
|
| 1159 |
-
try: await status.edit_text(f"❌ Voice error: {str(e)[:200]}")
|
| 1160 |
-
except: pass
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
@dp.message(F.photo)
|
| 1164 |
-
async def handle_photo(message: Message):
|
| 1165 |
-
"""Photo → AI vision + PIL fallback → respond."""
|
| 1166 |
-
if is_group(message) and not should_respond_in_group(message):
|
| 1167 |
-
return
|
| 1168 |
|
| 1169 |
-
status = await message.answer("
|
| 1170 |
try:
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
except: pass
|
| 1181 |
-
|
| 1182 |
-
await process_and_reply(message, caption, attachments=[{"type": "image", "b64": b64}])
|
| 1183 |
-
|
| 1184 |
except Exception as e:
|
| 1185 |
-
log.
|
| 1186 |
-
try: await status.edit_text(f"❌ Photo error: {str(e)[:200]}")
|
| 1187 |
-
except: pass
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
@dp.message(F.document)
|
| 1191 |
-
async def handle_document(message: Message):
|
| 1192 |
-
"""Documents → read → analyze."""
|
| 1193 |
-
if is_group(message) and not should_respond_in_group(message):
|
| 1194 |
-
return
|
| 1195 |
-
|
| 1196 |
-
doc = message.document
|
| 1197 |
-
if doc.file_size and doc.file_size > 15 * 1024 * 1024:
|
| 1198 |
-
return await message.answer("📁 File too large (max 15MB).")
|
| 1199 |
-
|
| 1200 |
-
status = await message.answer("📁 Reading file...")
|
| 1201 |
-
try:
|
| 1202 |
-
file = await bot.get_file(doc.file_id)
|
| 1203 |
-
fdata = await bot.download_file(file.file_path)
|
| 1204 |
-
content_bytes = fdata.read() if hasattr(fdata, 'read') else bytes(fdata)
|
| 1205 |
-
|
| 1206 |
-
fname = doc.file_name or "unknown"
|
| 1207 |
-
ext = fname.split(".")[-1].lower() if "." in fname else "txt"
|
| 1208 |
-
|
| 1209 |
-
# Image documents
|
| 1210 |
-
if ext in ("png", "jpg", "jpeg", "gif", "webp", "bmp"):
|
| 1211 |
-
b64 = base64.b64encode(content_bytes).decode()
|
| 1212 |
-
caption = message.caption or "Analyze this image."
|
| 1213 |
-
try: await status.delete()
|
| 1214 |
-
except: pass
|
| 1215 |
-
return await process_and_reply(message, caption, attachments=[{"type": "image", "b64": b64}])
|
| 1216 |
-
|
| 1217 |
-
# Text-based files
|
| 1218 |
try:
|
| 1219 |
-
|
| 1220 |
except:
|
| 1221 |
-
|
| 1222 |
-
|
| 1223 |
-
caption = message.caption or f"Analyze this {ext} file."
|
| 1224 |
-
msg = f"{caption}\n\nFile: {fname} ({len(content_bytes):,} bytes)\n\n```{ext}\n{content[:8000]}\n```"
|
| 1225 |
-
|
| 1226 |
-
# Save file locally for admin
|
| 1227 |
-
if is_admin(message.from_user.id):
|
| 1228 |
-
save_path = os.path.join(Config.FILES_DIR, fname)
|
| 1229 |
-
with open(save_path, "wb") as f: f.write(content_bytes)
|
| 1230 |
-
msg += f"\n\n💾 Saved to: {save_path}"
|
| 1231 |
-
|
| 1232 |
-
try: await status.delete()
|
| 1233 |
-
except: pass
|
| 1234 |
-
|
| 1235 |
-
await process_and_reply(message, msg)
|
| 1236 |
-
|
| 1237 |
-
except Exception as e:
|
| 1238 |
-
log.error(f"Document error: {e}")
|
| 1239 |
-
try: await status.edit_text(f"❌ File error: {str(e)[:200]}")
|
| 1240 |
-
except: pass
|
| 1241 |
-
|
| 1242 |
-
|
| 1243 |
-
@dp.message(F.sticker)
|
| 1244 |
-
async def handle_sticker(message: Message):
|
| 1245 |
-
if is_group(message) and not should_respond_in_group(message):
|
| 1246 |
-
return
|
| 1247 |
-
emoji = message.sticker.emoji or "😀"
|
| 1248 |
-
await process_and_reply(message, f"User sent a sticker: {emoji}. Respond playfully!")
|
| 1249 |
-
|
| 1250 |
-
|
| 1251 |
-
@dp.message(F.location)
|
| 1252 |
-
async def handle_location(message: Message):
|
| 1253 |
-
if is_group(message) and not should_respond_in_group(message):
|
| 1254 |
-
return
|
| 1255 |
-
lat, lon = message.location.latitude, message.location.longitude
|
| 1256 |
-
await process_and_reply(message,
|
| 1257 |
-
f"User shared their location: lat={lat}, lon={lon}. "
|
| 1258 |
-
"Look up what's nearby and provide useful information about this location.")
|
| 1259 |
-
|
| 1260 |
|
| 1261 |
-
@dp.message(F.
|
| 1262 |
-
async def
|
| 1263 |
if is_group(message) and not should_respond_in_group(message):
|
| 1264 |
return
|
| 1265 |
-
c = message.contact
|
| 1266 |
-
await process_and_reply(message,
|
| 1267 |
-
f"User shared a contact: {c.first_name} {c.last_name or ''}, phone: {c.phone_number}")
|
| 1268 |
-
|
| 1269 |
-
|
| 1270 |
-
# ╔═══════════════════════════════════════════════════════════════════╗
|
| 1271 |
-
# ║ 🔘 CALLBACK HANDLERS ║
|
| 1272 |
-
# ╚═══════════════════════════════════════════════════════════════════╝
|
| 1273 |
-
|
| 1274 |
-
@dp.callback_query(F.data.startswith("agent:"))
|
| 1275 |
-
async def cb_agent(cb: CallbackQuery):
|
| 1276 |
-
agent = cb.data.split(":")[1]
|
| 1277 |
-
state = get_state(cb.from_user.id)
|
| 1278 |
-
if agent == "auto":
|
| 1279 |
-
state["agent"] = None
|
| 1280 |
-
await cb.message.edit_text("🤖 Mode: <b>Auto</b> — Director decides which agents to use",
|
| 1281 |
-
parse_mode=ParseMode.HTML)
|
| 1282 |
-
else:
|
| 1283 |
-
state["agent"] = agent
|
| 1284 |
-
info = AGENTS.get(agent, {})
|
| 1285 |
-
await cb.message.edit_text(
|
| 1286 |
-
f"{info.get('icon','')} Agent: <b>{info.get('name', agent)}</b>\n\n"
|
| 1287 |
-
f"<i>{info.get('prompt','')[:200]}...</i>",
|
| 1288 |
-
parse_mode=ParseMode.HTML)
|
| 1289 |
-
await cb.answer("Switched!")
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
@dp.callback_query(F.data.startswith("model:"))
|
| 1293 |
-
async def cb_model(cb: CallbackQuery):
|
| 1294 |
-
model = cb.data.split(":", 1)[1]
|
| 1295 |
-
get_state(cb.from_user.id)["model"] = model
|
| 1296 |
-
DB.update_user(cb.from_user.id, preferred_model=model)
|
| 1297 |
-
await cb.message.edit_text(f"🔄 Model: <code>{model}</code>", parse_mode=ParseMode.HTML)
|
| 1298 |
-
await cb.answer("Model switched!")
|
| 1299 |
-
|
| 1300 |
-
|
| 1301 |
-
@dp.callback_query(F.data.startswith("nav:"))
|
| 1302 |
-
async def cb_nav(cb: CallbackQuery):
|
| 1303 |
-
page = cb.data.split(":")[1]
|
| 1304 |
-
if page == "main":
|
| 1305 |
-
await cb.message.edit_text("🏠 <b>Main Menu</b>", parse_mode=ParseMode.HTML, reply_markup=kb_main())
|
| 1306 |
-
elif page == "models":
|
| 1307 |
-
await cb.message.edit_text("🔄 <b>Select Model</b>", parse_mode=ParseMode.HTML, reply_markup=kb_models())
|
| 1308 |
-
elif page == "settings":
|
| 1309 |
-
await cb.message.edit_text("⚙️ <b>Settings</b>", parse_mode=ParseMode.HTML,
|
| 1310 |
-
reply_markup=kb_settings(cb.from_user.id))
|
| 1311 |
-
elif page == "stats":
|
| 1312 |
-
user = DB.get_user(cb.from_user.id)
|
| 1313 |
-
if user:
|
| 1314 |
-
await cb.message.edit_text(
|
| 1315 |
-
f"📊 <b>Stats</b>\n💬 {user['total_messages']} msgs | 🔤 {user['total_tokens']:,} tokens",
|
| 1316 |
-
parse_mode=ParseMode.HTML)
|
| 1317 |
-
await cb.answer()
|
| 1318 |
-
|
| 1319 |
-
|
| 1320 |
-
@dp.callback_query(F.data.startswith("set:"))
|
| 1321 |
-
async def cb_set(cb: CallbackQuery):
|
| 1322 |
-
action = cb.data.split(":")[1]
|
| 1323 |
-
if action == "temp":
|
| 1324 |
-
await cb.message.edit_text("🌡️ <b>Temperature</b>\nLow = precise, High = creative",
|
| 1325 |
-
parse_mode=ParseMode.HTML, reply_markup=kb_temp())
|
| 1326 |
-
elif action == "sysprompt":
|
| 1327 |
-
get_state(cb.from_user.id)["awaiting"] = "sysprompt"
|
| 1328 |
-
await cb.message.edit_text("📝 Send your custom system prompt now.\nSend 'clear' to reset.")
|
| 1329 |
-
elif action == "personas":
|
| 1330 |
-
rows = DB.execute("SELECT * FROM personas WHERE user_id=?", (cb.from_user.id,), fetch=True)
|
| 1331 |
-
if not rows:
|
| 1332 |
-
await cb.message.edit_text("No personas yet. Use /persona <name> <prompt>",
|
| 1333 |
-
parse_mode=ParseMode.HTML)
|
| 1334 |
-
else:
|
| 1335 |
-
kb_rows = [[InlineKeyboardButton(text=f"🎭 {r['name']}", callback_data=f"persona:{r['id']}")]
|
| 1336 |
-
for r in rows]
|
| 1337 |
-
await cb.message.edit_text("🎭 <b>Your Personas:</b>", parse_mode=ParseMode.HTML,
|
| 1338 |
-
reply_markup=InlineKeyboardMarkup(inline_keyboard=kb_rows))
|
| 1339 |
-
elif action == "reset":
|
| 1340 |
-
DB.update_user(cb.from_user.id, preferred_model=Config.DEFAULT_MODEL,
|
| 1341 |
-
temperature=0.7, system_prompt="", max_tokens=4096)
|
| 1342 |
-
get_state(cb.from_user.id).update({"model": None, "agent": None})
|
| 1343 |
-
await cb.message.edit_text("✅ All settings reset to defaults.")
|
| 1344 |
-
await cb.answer()
|
| 1345 |
-
|
| 1346 |
-
|
| 1347 |
-
@dp.callback_query(F.data.startswith("temp:"))
|
| 1348 |
-
async def cb_temp(cb: CallbackQuery):
|
| 1349 |
-
t = float(cb.data.split(":")[1])
|
| 1350 |
-
DB.update_user(cb.from_user.id, temperature=t)
|
| 1351 |
-
await cb.message.edit_text(f"🌡️ Temperature set to <b>{t}</b>", parse_mode=ParseMode.HTML)
|
| 1352 |
-
await cb.answer()
|
| 1353 |
-
|
| 1354 |
-
|
| 1355 |
-
@dp.callback_query(F.data.startswith("fb:"))
|
| 1356 |
-
async def cb_feedback(cb: CallbackQuery):
|
| 1357 |
-
action = cb.data.split(":")[1]
|
| 1358 |
-
if action == "up":
|
| 1359 |
-
DB.execute("INSERT INTO feedback (user_id,rating) VALUES (?,5)", (cb.from_user.id,))
|
| 1360 |
-
await cb.answer("👍 Thanks!")
|
| 1361 |
-
elif action == "down":
|
| 1362 |
-
DB.execute("INSERT INTO feedback (user_id,rating) VALUES (?,1)", (cb.from_user.id,))
|
| 1363 |
-
await cb.answer("👎 We'll improve!")
|
| 1364 |
-
elif action == "retry":
|
| 1365 |
-
state = get_state(cb.from_user.id)
|
| 1366 |
-
if state.get("last_message"):
|
| 1367 |
-
await cb.answer("🔄 Retrying...")
|
| 1368 |
-
# Re-process
|
| 1369 |
-
await process_and_reply(cb.message, state["last_message"])
|
| 1370 |
-
else:
|
| 1371 |
-
await cb.answer("No message to retry")
|
| 1372 |
-
elif action == "speak":
|
| 1373 |
-
state = get_state(cb.from_user.id)
|
| 1374 |
-
resp = state.get("last_response")
|
| 1375 |
-
if resp and resp.get("text"):
|
| 1376 |
-
await cb.answer("🔊 Generating audio...")
|
| 1377 |
-
try:
|
| 1378 |
-
audio = await llm.tts(resp["text"][:4000])
|
| 1379 |
-
if audio:
|
| 1380 |
-
path = os.path.join(Config.DATA_DIR, f"tts_{int(time.time())}.mp3")
|
| 1381 |
-
with open(path, "wb") as f: f.write(audio)
|
| 1382 |
-
await cb.message.answer_voice(voice=FSInputFile(path))
|
| 1383 |
-
try: os.unlink(path)
|
| 1384 |
-
except: pass
|
| 1385 |
-
else:
|
| 1386 |
-
await cb.message.answer("❌ TTS unavailable")
|
| 1387 |
-
except Exception as e:
|
| 1388 |
-
await cb.message.answer(f"❌ TTS error: {str(e)[:100]}")
|
| 1389 |
-
else:
|
| 1390 |
-
await cb.answer("Nothing to read")
|
| 1391 |
-
|
| 1392 |
-
|
| 1393 |
-
@dp.callback_query(F.data.startswith("mem:"))
|
| 1394 |
-
async def cb_memory(cb: CallbackQuery):
|
| 1395 |
-
action = cb.data.split(":")[1]
|
| 1396 |
-
uid = cb.from_user.id
|
| 1397 |
-
if action == "view":
|
| 1398 |
-
r = await memory.recall(uid, "everything about me", 10)
|
| 1399 |
-
text = "🧠 <b>Your Memories:</b>\n\n" + ("\n".join(f"• {x}" for x in r) if r else "Empty")
|
| 1400 |
-
await cb.message.edit_text(text[:4000], parse_mode=ParseMode.HTML)
|
| 1401 |
-
elif action == "search":
|
| 1402 |
-
get_state(uid)["awaiting"] = "mem_search"
|
| 1403 |
-
await cb.message.edit_text("🔍 Send your search query:")
|
| 1404 |
-
elif action == "clear":
|
| 1405 |
-
await cb.message.edit_text("⚠️ Clear ALL memories?",
|
| 1406 |
-
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
| 1407 |
-
[InlineKeyboardButton(text="✅ Yes", callback_data="mem:do_clear"),
|
| 1408 |
-
InlineKeyboardButton(text="❌ No", callback_data="nav:main")]]))
|
| 1409 |
-
elif action == "do_clear":
|
| 1410 |
-
try:
|
| 1411 |
-
if memory.collection:
|
| 1412 |
-
r = memory.collection.get(where={"user_id": str(uid)})
|
| 1413 |
-
if r and r["ids"]: memory.collection.delete(ids=r["ids"])
|
| 1414 |
-
except: pass
|
| 1415 |
-
DB.execute("DELETE FROM memories WHERE user_id=?", (uid,))
|
| 1416 |
-
await cb.message.edit_text("🗑️ All memories cleared.")
|
| 1417 |
-
await cb.answer()
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
@dp.callback_query(F.data.startswith("persona:"))
|
| 1421 |
-
async def cb_persona(cb: CallbackQuery):
|
| 1422 |
-
val = cb.data.split(":")[1]
|
| 1423 |
-
if val == "clear":
|
| 1424 |
-
DB.execute("DELETE FROM personas WHERE user_id=?", (cb.from_user.id,))
|
| 1425 |
-
await cb.message.edit_text("🗑️ All personas deleted.")
|
| 1426 |
-
else:
|
| 1427 |
-
pid = int(val)
|
| 1428 |
-
row = DB.execute("SELECT * FROM personas WHERE id=? AND user_id=?",
|
| 1429 |
-
(pid, cb.from_user.id), fetchone=True)
|
| 1430 |
-
if row:
|
| 1431 |
-
DB.update_user(cb.from_user.id, system_prompt=row["prompt"])
|
| 1432 |
-
await cb.message.edit_text(f"🎭 Activated: <b>{row['name']}</b>", parse_mode=ParseMode.HTML)
|
| 1433 |
-
await cb.answer()
|
| 1434 |
-
|
| 1435 |
-
|
| 1436 |
-
@dp.callback_query(F.data.startswith("health:"))
|
| 1437 |
-
async def cb_health(cb: CallbackQuery):
|
| 1438 |
-
action = cb.data.split(":")[1]
|
| 1439 |
-
if action == "refresh":
|
| 1440 |
-
h = await doctor.check_health()
|
| 1441 |
-
await cb.message.edit_text(doctor.format_report(h), reply_markup=kb_health())
|
| 1442 |
-
elif action == "logs":
|
| 1443 |
-
rows = DB.execute("SELECT * FROM health_logs ORDER BY created_at DESC LIMIT 5", fetch=True)
|
| 1444 |
-
text = "📋 <b>Health Logs:</b>\n\n"
|
| 1445 |
-
for r in rows:
|
| 1446 |
-
text += f"• {r['status']} | {r['created_at']}\n"
|
| 1447 |
-
await cb.message.edit_text(text[:4000], parse_mode=ParseMode.HTML)
|
| 1448 |
-
await cb.answer()
|
| 1449 |
-
|
| 1450 |
-
|
| 1451 |
-
@dp.callback_query(F.data.startswith("adm:"))
|
| 1452 |
-
async def cb_admin(cb: CallbackQuery):
|
| 1453 |
-
if not is_admin(cb.from_user.id):
|
| 1454 |
-
return await cb.answer("🚫 Admin only")
|
| 1455 |
-
action = cb.data.split(":")[1]
|
| 1456 |
-
if action == "stats":
|
| 1457 |
-
stats = DB.user_stats()
|
| 1458 |
-
await cb.message.edit_text(
|
| 1459 |
-
f"📊 <b>Full Stats</b>\n\n"
|
| 1460 |
-
f"👥 Users: {stats['total']}\n📈 Active today: {stats['active_today']}\n"
|
| 1461 |
-
f"💬 Total msgs: {stats['total_messages']:,}\n🔤 Tokens: {stats['total_tokens']:,}",
|
| 1462 |
-
parse_mode=ParseMode.HTML, reply_markup=kb_admin())
|
| 1463 |
-
elif action == "health":
|
| 1464 |
-
h = await doctor.check_health()
|
| 1465 |
-
await cb.message.edit_text(doctor.format_report(h), reply_markup=kb_health())
|
| 1466 |
-
elif action == "toologs":
|
| 1467 |
-
rows = DB.execute("SELECT * FROM tool_usage ORDER BY created_at DESC LIMIT 15", fetch=True)
|
| 1468 |
-
text = "📜 <b>Tool Logs:</b>\n"
|
| 1469 |
-
for r in rows:
|
| 1470 |
-
s = "✅" if r["success"] else "❌"
|
| 1471 |
-
text += f"\n{s} <code>{r['tool_name']}</code> ({r['agent_name']}) {r['execution_time']:.1f}s"
|
| 1472 |
-
await cb.message.edit_text(text[:4000], parse_mode=ParseMode.HTML, reply_markup=kb_admin())
|
| 1473 |
-
elif action == "bots":
|
| 1474 |
-
r = await tools.execute("manage_bots", {"action": "list"}, cb.from_user.id)
|
| 1475 |
-
await cb.message.edit_text(r[:4000], reply_markup=kb_admin())
|
| 1476 |
-
elif action == "restart":
|
| 1477 |
-
await cb.message.edit_text("🔄 Restarting...")
|
| 1478 |
-
await asyncio.sleep(2)
|
| 1479 |
-
os.execv(sys.executable, [sys.executable] + sys.argv)
|
| 1480 |
-
elif action == "backup":
|
| 1481 |
-
if os.path.exists(DB_PATH := os.path.join(Config.DATA_DIR, "agentforge.db")):
|
| 1482 |
-
await cb.message.answer_document(
|
| 1483 |
-
FSInputFile(DB_PATH), caption="💾 Database Backup")
|
| 1484 |
-
await cb.answer("Backup sent!")
|
| 1485 |
-
await cb.answer()
|
| 1486 |
|
|
|
|
|
|
|
| 1487 |
|
| 1488 |
-
#
|
| 1489 |
-
|
| 1490 |
-
|
|
|
|
|
|
|
|
|
|
| 1491 |
|
| 1492 |
-
|
| 1493 |
-
|
| 1494 |
-
query = inline_query.query
|
| 1495 |
-
if not query or len(query) < 3: return
|
| 1496 |
|
| 1497 |
try:
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
|
| 1504 |
-
results = [InlineQueryResultArticle(
|
| 1505 |
-
id=hashlib.md5(query.encode()).hexdigest()[:16],
|
| 1506 |
-
title=f"🤖 {query[:50]}",
|
| 1507 |
-
description=text[:100],
|
| 1508 |
-
input_message_content=InputTextMessageContent(
|
| 1509 |
-
message_text=f"❓ <b>{query}</b>\n\n🤖 {text}",
|
| 1510 |
-
parse_mode=ParseMode.HTML))]
|
| 1511 |
-
await inline_query.answer(results, cache_time=30)
|
| 1512 |
-
except: pass
|
| 1513 |
-
|
| 1514 |
-
|
| 1515 |
-
# ╔═══════════════════════════════════════════════════════════════════╗
|
| 1516 |
-
# ║ 🏥 DOCTOR BACKGROUND ALERT SYSTEM ║
|
| 1517 |
-
# ╚═══════════════════════════════════════════════════════════════════╝
|
| 1518 |
-
|
| 1519 |
-
async def doctor_alert_callback():
|
| 1520 |
-
"""Called periodically to check if doctor found critical issues."""
|
| 1521 |
-
if doctor.issues and Config.ADMIN_IDS:
|
| 1522 |
-
critical = [i for i in doctor.issues if any(w in i.lower() for w in ["disk", "ram", "database"])]
|
| 1523 |
-
if critical:
|
| 1524 |
-
alert = "🚨 <b>DOCTOR ALERT</b>\n\n" + "\n".join(f"⚠️ {i}" for i in critical)
|
| 1525 |
-
for admin_id in Config.ADMIN_IDS:
|
| 1526 |
-
try:
|
| 1527 |
-
await bot.send_message(admin_id, alert, parse_mode=ParseMode.HTML)
|
| 1528 |
-
except: pass
|
| 1529 |
-
|
| 1530 |
-
|
| 1531 |
-
# ╔═══════════════════════════════════════════════════════════════════╗
|
| 1532 |
-
# ║ 🚀 STARTUP & MAIN ║
|
| 1533 |
-
# ╚═══════════════════════════════════════════════════════════════════╝
|
| 1534 |
|
| 1535 |
-
|
| 1536 |
-
|
|
|
|
| 1537 |
|
| 1538 |
-
|
|
|
|
| 1539 |
try:
|
| 1540 |
-
commands = [
|
| 1541 |
-
BotCommand(command="start", description="🚀 Start bot"),
|
| 1542 |
-
BotCommand(command="help", description="📚 Help menu"),
|
| 1543 |
-
BotCommand(command="agent", description="🤖 Switch agent"),
|
| 1544 |
-
BotCommand(command="model", description="🔄 Switch model"),
|
| 1545 |
-
BotCommand(command="search", description="🔍 Web search"),
|
| 1546 |
-
BotCommand(command="code", description="💻 Run code"),
|
| 1547 |
-
BotCommand(command="image", description="🎨 Generate image"),
|
| 1548 |
-
BotCommand(command="tts", description="🔊 Text to speech"),
|
| 1549 |
-
BotCommand(command="memory", description="🧠 Memory"),
|
| 1550 |
-
BotCommand(command="clear", description="🗑️ Clear chat"),
|
| 1551 |
-
BotCommand(command="settings", description="⚙️ Settings"),
|
| 1552 |
-
BotCommand(command="tools", description="🛠️ List tools"),
|
| 1553 |
-
BotCommand(command="stats", description="📊 Statistics"),
|
| 1554 |
-
BotCommand(command="self", description="🧬 Self-modify"),
|
| 1555 |
-
BotCommand(command="health", description="🏥 Health check"),
|
| 1556 |
-
BotCommand(command="help", description="📚 All commands"),
|
| 1557 |
-
]
|
| 1558 |
-
await bot.set_my_commands(commands)
|
| 1559 |
-
|
| 1560 |
-
# Get bot username for group mentions
|
| 1561 |
me = await bot.get_me()
|
| 1562 |
Config.BOT_USERNAME = me.username or ""
|
| 1563 |
-
log.info(f"
|
| 1564 |
except Exception as e:
|
| 1565 |
-
log.warning(f"
|
| 1566 |
-
|
| 1567 |
-
# Models
|
| 1568 |
-
models = llm.available_models()
|
| 1569 |
-
log.info(f"✅ Models available: {len(models)}")
|
| 1570 |
-
for m in models: log.info(f" • {m}")
|
| 1571 |
-
|
| 1572 |
-
# Start Doctor (always-alive health monitor)
|
| 1573 |
-
await doctor.start()
|
| 1574 |
-
log.info("✅ Doctor started (monitoring every {Config.HEALTH_INTERVAL}s)")
|
| 1575 |
-
|
| 1576 |
-
# Schedule periodic doctor alerts
|
| 1577 |
-
scheduler.add_job(doctor_alert_callback, 'interval', minutes=10)
|
| 1578 |
-
scheduler.start()
|
| 1579 |
-
log.info("✅ Scheduler started")
|
| 1580 |
|
| 1581 |
-
#
|
| 1582 |
-
|
| 1583 |
-
|
| 1584 |
-
await bot.send_message(admin_id,
|
| 1585 |
-
f"🚀 <b>AgentForge Online!</b>\n\n"
|
| 1586 |
-
f"🤖 @{Config.BOT_USERNAME}\n"
|
| 1587 |
-
f"🔄 Models: {len(models)}\n"
|
| 1588 |
-
f"🛠️ Tools: {len(ALL_TOOLS_SCHEMA)}\n"
|
| 1589 |
-
f"🤝 Agents: {len(AGENTS)}\n"
|
| 1590 |
-
f"🏥 Doctor: Active\n"
|
| 1591 |
-
f"⏱️ Boot: {datetime.now().strftime('%H:%M:%S')}",
|
| 1592 |
-
parse_mode=ParseMode.HTML)
|
| 1593 |
-
except: pass
|
| 1594 |
-
|
| 1595 |
-
log.info("🎉 AgentForge v2 fully operational!")
|
| 1596 |
-
|
| 1597 |
-
|
| 1598 |
-
async def on_shutdown():
|
| 1599 |
-
log.info("Shutting down...")
|
| 1600 |
-
await doctor.stop()
|
| 1601 |
-
scheduler.shutdown(wait=False)
|
| 1602 |
-
for h, info in list(tools.spawned_bots.items()):
|
| 1603 |
-
try:
|
| 1604 |
-
info["task"].cancel()
|
| 1605 |
-
await info["bot"].session.close()
|
| 1606 |
-
except: pass
|
| 1607 |
-
|
| 1608 |
-
|
| 1609 |
-
async def main():
|
| 1610 |
-
await on_startup()
|
| 1611 |
|
| 1612 |
try:
|
| 1613 |
await bot.delete_webhook(drop_pending_updates=True)
|
| 1614 |
-
except
|
| 1615 |
-
|
| 1616 |
-
|
| 1617 |
-
log.info("📡 Starting polling...")
|
| 1618 |
-
try:
|
| 1619 |
-
await dp.start_polling(bot)
|
| 1620 |
-
finally:
|
| 1621 |
-
await on_shutdown()
|
| 1622 |
|
|
|
|
|
|
|
| 1623 |
|
| 1624 |
if __name__ == "__main__":
|
| 1625 |
-
|
| 1626 |
-
asyncio.run(main())
|
| 1627 |
-
except KeyboardInterrupt:
|
| 1628 |
-
log.info("Bot stopped by user")
|
| 1629 |
-
except Exception as e:
|
| 1630 |
-
log.critical(f"Fatal: {e}", exc_info=True)
|
| 1631 |
-
sys.exit(1)
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
import os
|
| 5 |
+
import re
|
| 6 |
+
import subprocess
|
| 7 |
+
import threading
|
| 8 |
+
import time
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
from urllib.parse import urlparse
|
|
|
|
| 10 |
|
|
|
|
| 11 |
from flask import Flask, request, Response
|
| 12 |
|
| 13 |
+
from aiogram import Bot, Dispatcher, F, types
|
|
|
|
| 14 |
from aiogram.filters import CommandStart, Command
|
| 15 |
+
from aiogram.enums import ParseMode, ChatType, ChatAction
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
from aiogram.client.session.aiohttp import AiohttpSession
|
| 17 |
from aiogram.client.telegram import TelegramAPIServer
|
|
|
|
| 18 |
|
| 19 |
+
from agent import Config, DB, Permission, tools, engine, scheduler, live_log, TOOLS_SCHEMA
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
|
| 22 |
+
log = logging.getLogger("app")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
+
# =========================================================
|
| 25 |
+
# HF CURL BRIDGE (Telegram API proxy)
|
| 26 |
+
# =========================================================
|
| 27 |
|
| 28 |
+
BRIDGE_PORT = int(os.getenv("BRIDGE_PORT", "7860"))
|
| 29 |
+
PROXY_TARGET = os.getenv("PROXY_TARGET", "https://api.telegram.org") # ideally your Cloudflare Worker
|
| 30 |
+
CLOUDFLARE_IP = os.getenv("CLOUDFLARE_IP", "") # optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
flask_app = Flask(__name__)
|
| 33 |
+
BOOT = time.time()
|
| 34 |
|
| 35 |
+
@flask_app.route("/")
|
| 36 |
def health():
|
|
|
|
|
|
|
|
|
|
| 37 |
return json.dumps({
|
| 38 |
+
"status": "ok",
|
| 39 |
+
"uptime_sec": int(time.time() - BOOT),
|
| 40 |
+
"bot_username": Config.BOT_USERNAME,
|
| 41 |
+
"admin_ids": Config.ADMIN_IDS,
|
|
|
|
|
|
|
| 42 |
}), 200, {"Content-Type": "application/json"}
|
| 43 |
|
| 44 |
+
def run_curl(method: str, url: str, data=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
parsed = urlparse(url)
|
| 46 |
domain = parsed.hostname
|
| 47 |
|
| 48 |
cmd = ["curl", "-X", method, url]
|
| 49 |
|
| 50 |
+
if CLOUDFLARE_IP and domain:
|
| 51 |
+
cmd += ["--resolve", f"{domain}:443:{CLOUDFLARE_IP}"]
|
|
|
|
| 52 |
|
| 53 |
cmd += [
|
| 54 |
+
"-H", "User-Agent: Mozilla/5.0",
|
| 55 |
"-H", "Content-Type: application/json",
|
| 56 |
+
"-k", "-s", "--max-time", "30"
|
| 57 |
]
|
| 58 |
|
| 59 |
+
stdin_payload = None
|
| 60 |
+
if data is not None:
|
| 61 |
+
cmd += ["--data-binary", "@-"]
|
| 62 |
+
stdin_payload = json.dumps(data)
|
| 63 |
|
| 64 |
try:
|
| 65 |
+
r = subprocess.run(cmd, input=stdin_payload, capture_output=True, text=True, timeout=35)
|
| 66 |
+
if not r.stdout:
|
| 67 |
+
return json.dumps({"ok": False, "description": f"Empty curl response: {r.stderr[:200]}"})
|
| 68 |
+
return r.stdout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
except Exception as e:
|
| 70 |
return json.dumps({"ok": False, "description": str(e)[:300]})
|
| 71 |
|
| 72 |
+
@flask_app.route("/bot<token>/<method>", methods=["POST", "GET"])
|
|
|
|
| 73 |
def proxy(token, method):
|
| 74 |
+
real_url = f"{PROXY_TARGET}/bot{token}/{method}"
|
|
|
|
|
|
|
|
|
|
| 75 |
data = request.get_json(force=True, silent=True)
|
| 76 |
if not data:
|
| 77 |
data = request.form.to_dict() or request.args.to_dict() or None
|
| 78 |
+
out = run_curl(request.method, real_url, data)
|
| 79 |
+
return Response(out, mimetype="application/json")
|
|
|
|
|
|
|
| 80 |
|
| 81 |
def start_bridge():
|
| 82 |
+
werk = logging.getLogger("werkzeug")
|
| 83 |
+
werk.setLevel(logging.WARNING)
|
| 84 |
+
log.info(f"Bridge running on 0.0.0.0:{BRIDGE_PORT} → {PROXY_TARGET} (resolve_ip={bool(CLOUDFLARE_IP)})")
|
| 85 |
+
flask_app.run(host="0.0.0.0", port=BRIDGE_PORT, threaded=True)
|
|
|
|
|
|
|
|
|
|
| 86 |
|
|
|
|
| 87 |
threading.Thread(target=start_bridge, daemon=True).start()
|
| 88 |
+
time.sleep(2)
|
|
|
|
|
|
|
| 89 |
|
| 90 |
+
# =========================================================
|
| 91 |
+
# Telegram Bot (Aiogram)
|
| 92 |
+
# =========================================================
|
| 93 |
|
| 94 |
+
session = AiohttpSession(api=TelegramAPIServer.from_base(f"http://127.0.0.1:{BRIDGE_PORT}"))
|
|
|
|
|
|
|
| 95 |
bot = Bot(token=Config.BOT_TOKEN, session=session)
|
| 96 |
dp = Dispatcher()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
+
def is_group(m: types.Message) -> bool:
|
| 99 |
+
return m.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP)
|
| 100 |
|
| 101 |
+
def should_respond_in_group(m: types.Message) -> bool:
|
| 102 |
+
# Reply to bot message
|
| 103 |
+
if m.reply_to_message and m.reply_to_message.from_user and m.reply_to_message.from_user.id == bot.id:
|
| 104 |
return True
|
| 105 |
|
| 106 |
+
# @mention by username
|
| 107 |
+
uname = (Config.BOT_USERNAME or "").lower()
|
| 108 |
+
if uname and m.text and f"@{uname}" in m.text.lower():
|
| 109 |
+
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
# entity mention
|
| 112 |
+
if uname and m.entities and m.text:
|
| 113 |
+
for e in m.entities:
|
| 114 |
if e.type == "mention":
|
| 115 |
+
mention = m.text[e.offset:e.offset + e.length].lower()
|
| 116 |
+
if mention == f"@{uname}":
|
| 117 |
return True
|
| 118 |
|
| 119 |
return False
|
| 120 |
|
| 121 |
+
def strip_mention(text: str) -> str:
|
| 122 |
+
if not text:
|
| 123 |
+
return text
|
| 124 |
+
uname = Config.BOT_USERNAME
|
| 125 |
+
if not uname:
|
| 126 |
+
return text
|
| 127 |
+
return re.sub(rf"@{re.escape(uname)}", "", text, flags=re.IGNORECASE).strip()
|
| 128 |
+
|
| 129 |
+
async def safe_send(chat_id: int, text: str):
|
| 130 |
+
if not text:
|
| 131 |
+
text = "OK"
|
| 132 |
+
# Telegram message limit ~4096
|
| 133 |
+
chunks = [text[i:i+3500] for i in range(0, len(text), 3500)]
|
| 134 |
+
for c in chunks:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
try:
|
| 136 |
+
await bot.send_message(chat_id, c, parse_mode=ParseMode.HTML, disable_web_page_preview=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
except:
|
| 138 |
+
await bot.send_message(chat_id, c, disable_web_page_preview=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
+
# =========================================================
|
| 141 |
+
# Scheduler hookup (24/7 autonomous tasks)
|
| 142 |
+
# =========================================================
|
| 143 |
|
| 144 |
+
async def scheduler_sender(chat_id: int, text: str):
|
| 145 |
+
await safe_send(chat_id, text)
|
|
|
|
| 146 |
|
| 147 |
+
# =========================================================
|
| 148 |
+
# Commands
|
| 149 |
+
# =========================================================
|
| 150 |
|
| 151 |
+
@dp.message(CommandStart())
|
| 152 |
+
async def start_cmd(message: types.Message):
|
| 153 |
+
DB.upsert_user(message.from_user.id, message.from_user.username or "", message.from_user.first_name or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
await message.answer(
|
| 155 |
+
"<b>AgentForge (test build) online.</b>\n\n"
|
| 156 |
+
"Group mode: reply to me or @mention me.\n\n"
|
| 157 |
+
"Try:\n"
|
| 158 |
+
"• /tools\n"
|
| 159 |
+
"• /logs\n"
|
| 160 |
+
"• /remind 20 check oven\n"
|
| 161 |
+
"• Ask: 'search for x' or 'system info'\n",
|
| 162 |
+
parse_mode=ParseMode.HTML
|
| 163 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
@dp.message(Command("tools"))
|
| 166 |
+
async def tools_cmd(message: types.Message):
|
| 167 |
+
names = [t["function"]["name"] for t in TOOLS_SCHEMA]
|
| 168 |
+
await message.answer("<b>Tools:</b>\n" + "\n".join(f"• <code>{n}</code>" for n in names),
|
| 169 |
+
parse_mode=ParseMode.HTML)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
|
| 171 |
+
@dp.message(Command("logs"))
|
| 172 |
+
async def logs_cmd(message: types.Message):
|
| 173 |
+
if not Config.is_admin(message.from_user.id):
|
| 174 |
+
return await message.answer("Admin only.")
|
| 175 |
+
await message.answer("<b>Live Logs</b>\n\n<pre>" + live_log.format(60) + "</pre>",
|
| 176 |
+
parse_mode=ParseMode.HTML)
|
| 177 |
+
|
| 178 |
+
@dp.message(Command("tasks"))
|
| 179 |
+
async def tasks_cmd(message: types.Message):
|
| 180 |
+
if not Config.is_admin(message.from_user.id):
|
| 181 |
+
return await message.answer("Admin only.")
|
| 182 |
+
out = await tools.run(message.from_user.id, "list_tasks", {}, is_group=is_group(message))
|
| 183 |
+
await message.answer(f"<pre>{out}</pre>", parse_mode=ParseMode.HTML)
|
| 184 |
+
|
| 185 |
+
@dp.message(Command("canceltask"))
|
| 186 |
+
async def canceltask_cmd(message: types.Message):
|
| 187 |
+
if not Config.is_admin(message.from_user.id):
|
| 188 |
+
return await message.answer("Admin only.")
|
| 189 |
+
parts = message.text.split()
|
| 190 |
if len(parts) < 2:
|
| 191 |
+
return await message.answer("Usage: /canceltask <id>")
|
| 192 |
+
try:
|
| 193 |
+
tid = int(parts[1])
|
| 194 |
+
except:
|
| 195 |
+
return await message.answer("Invalid id.")
|
| 196 |
+
out = await tools.run(message.from_user.id, "cancel_task", {"task_id": tid}, is_group=is_group(message))
|
| 197 |
+
await message.answer(out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
+
@dp.message(Command("remind"))
|
| 200 |
+
async def remind_cmd(message: types.Message):
|
| 201 |
+
"""
|
| 202 |
+
/remind 20 do something
|
| 203 |
+
Schedules a task that will fire and message you.
|
| 204 |
+
"""
|
| 205 |
+
if not Config.is_admin(message.from_user.id):
|
| 206 |
+
return await message.answer("Admin only (for now).")
|
| 207 |
parts = message.text.split(maxsplit=2)
|
| 208 |
if len(parts) < 3:
|
| 209 |
+
return await message.answer("Usage: /remind <seconds> <message>")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
try:
|
| 211 |
+
sec = int(parts[1])
|
| 212 |
+
except:
|
| 213 |
+
return await message.answer("Seconds must be an integer.")
|
| 214 |
+
reminder_text = parts[2].strip()
|
| 215 |
+
prompt = f"Send a reminder to the user now. Reminder text: {reminder_text}"
|
| 216 |
+
out = await tools.run(message.from_user.id, "schedule_task",
|
| 217 |
+
{"delay_seconds": sec, "prompt": prompt, "repeat_seconds": 0},
|
| 218 |
+
is_group=is_group(message))
|
| 219 |
+
await message.answer(out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
+
@dp.message(Command("model"))
|
| 222 |
+
async def model_cmd(message: types.Message):
|
| 223 |
+
if not Config.is_admin(message.from_user.id):
|
| 224 |
+
return await message.answer("Admin only (for now).")
|
| 225 |
+
parts = message.text.split(maxsplit=1)
|
| 226 |
+
if len(parts) < 2:
|
| 227 |
+
return await message.answer(f"Usage: /model <model_name>\nCurrent default: {Config.DEFAULT_MODEL}")
|
| 228 |
+
new_model = parts[1].strip()
|
| 229 |
+
DB.q("UPDATE users SET preferred_model=? WHERE telegram_id=?", (new_model, message.from_user.id))
|
| 230 |
+
await message.answer(f"Model set to: <code>{new_model}</code>", parse_mode=ParseMode.HTML)
|
| 231 |
|
| 232 |
+
# =========================================================
|
| 233 |
+
# Main message handler
|
| 234 |
+
# =========================================================
|
| 235 |
|
| 236 |
@dp.message(F.text & ~F.text.startswith("/"))
|
| 237 |
+
async def on_text(message: types.Message):
|
|
|
|
|
|
|
| 238 |
if is_group(message) and not should_respond_in_group(message):
|
| 239 |
return
|
| 240 |
|
| 241 |
+
uid = message.from_user.id
|
| 242 |
+
DB.upsert_user(uid, message.from_user.username or "", message.from_user.first_name or "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
+
text = strip_mention(message.text or "")
|
| 245 |
|
| 246 |
+
# show typing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
try:
|
| 248 |
+
await bot.send_chat_action(message.chat.id, ChatAction.TYPING)
|
| 249 |
+
except:
|
| 250 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
+
status = await message.answer("💭 Thinking...")
|
| 253 |
try:
|
| 254 |
+
result = await engine.run(uid, message.chat.id, text, is_group=is_group(message))
|
| 255 |
+
reply = result.get("text", "OK")
|
| 256 |
+
footer = ""
|
| 257 |
+
if result.get("tools_used"):
|
| 258 |
+
footer += "\n\n<i>Tools:</i> " + ", ".join(list(dict.fromkeys(result["tools_used"]))[:10])
|
| 259 |
+
footer += f"\n<i>Model:</i> {result.get('model','')}"
|
| 260 |
+
footer += f" | <i>Tokens:</i> {result.get('tokens',0)}"
|
| 261 |
+
await status.delete()
|
| 262 |
+
await safe_send(message.chat.id, reply + footer)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
except Exception as e:
|
| 264 |
+
log.exception("engine error")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
try:
|
| 266 |
+
await status.edit_text(f"❌ Error: {str(e)[:200]}")
|
| 267 |
except:
|
| 268 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
|
| 270 |
+
@dp.message(F.photo)
|
| 271 |
+
async def on_photo(message: types.Message):
|
| 272 |
if is_group(message) and not should_respond_in_group(message):
|
| 273 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
+
uid = message.from_user.id
|
| 276 |
+
DB.upsert_user(uid, message.from_user.username or "", message.from_user.first_name or "")
|
| 277 |
|
| 278 |
+
# Download largest photo
|
| 279 |
+
photo = message.photo[-1]
|
| 280 |
+
file = await bot.get_file(photo.file_id)
|
| 281 |
+
data = await bot.download_file(file.file_path)
|
| 282 |
+
img_bytes = data.read() if hasattr(data, "read") else bytes(data)
|
| 283 |
+
b64 = base64.b64encode(img_bytes).decode()
|
| 284 |
|
| 285 |
+
caption = strip_mention(message.caption or "Analyze this image.")
|
| 286 |
+
status = await message.answer("👁️ Processing photo...")
|
|
|
|
|
|
|
| 287 |
|
| 288 |
try:
|
| 289 |
+
result = await engine.run(uid, message.chat.id, caption, is_group=is_group(message),
|
| 290 |
+
attachments=[{"type":"image","b64":b64}])
|
| 291 |
+
await status.delete()
|
| 292 |
+
await safe_send(message.chat.id, result.get("text","OK"))
|
| 293 |
+
except Exception as e:
|
| 294 |
+
await status.edit_text(f"❌ Error: {str(e)[:200]}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
+
# =========================================================
|
| 297 |
+
# Startup / main
|
| 298 |
+
# =========================================================
|
| 299 |
|
| 300 |
+
async def main():
|
| 301 |
+
# set username for group mentions
|
| 302 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
me = await bot.get_me()
|
| 304 |
Config.BOT_USERNAME = me.username or ""
|
| 305 |
+
log.info(f"Bot username: @{Config.BOT_USERNAME}")
|
| 306 |
except Exception as e:
|
| 307 |
+
log.warning(f"Could not get bot username: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
+
# start scheduler
|
| 310 |
+
scheduler.set_sender(scheduler_sender)
|
| 311 |
+
await scheduler.start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
try:
|
| 314 |
await bot.delete_webhook(drop_pending_updates=True)
|
| 315 |
+
except:
|
| 316 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
+
log.info("Polling start")
|
| 319 |
+
await dp.start_polling(bot)
|
| 320 |
|
| 321 |
if __name__ == "__main__":
|
| 322 |
+
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|