Bjo53 commited on
Commit
ed6c496
·
verified ·
1 Parent(s): fde521f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +216 -1525
app.py CHANGED
@@ -1,1631 +1,322 @@
1
- """
2
- ╔══════════════════════════════════════════════════════════════════════════╗
3
- ║ AGENTFORGE v2 — Full Agentic Telegram Bot (Hugging Face Ready) ║
4
- ║ ║
5
- ║ • Curl Bridge bypasses HF network restrictions ║
6
- ║ • 10 coordinated AI agents working as a team ║
7
- ║ • 30+ tools with full system access ║
8
- ║ • Always-alive Doctor health monitor ║
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
- # ── Aiogram ──
26
- from aiogram import Bot, Dispatcher, types, F
27
  from aiogram.filters import CommandStart, Command
28
- from aiogram.types import (
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
- # ── Our Brain ──
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
- level=logging.INFO,
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
- # ║ 🌉 CURL BRIDGE BYPASSES HF NETWORK BLOCK ║
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": "✅ AgentForge Running",
75
- "uptime": f"{h}h {mins}m {s}s",
76
- "agents": len(AGENTS),
77
- "tools": len(ALL_TOOLS_SCHEMA),
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
- # Cloudflare IP resolve trick
106
- if Config.CLOUDFLARE_IP and domain:
107
- cmd += ["--resolve", f"{domain}:443:{Config.CLOUDFLARE_IP}"]
108
 
109
  cmd += [
110
- "-H", "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
111
  "-H", "Content-Type: application/json",
112
- "-k", "-s", "--max-time", "30",
113
  ]
114
 
115
- input_str = None
116
- if data:
117
- cmd += ["--data-binary", "@-"] # Read body from stdin
118
- input_str = json.dumps(data)
119
 
120
  try:
121
- result = subprocess.run(
122
- cmd, input=input_str,
123
- capture_output=True, text=True, timeout=35
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
- """Proxy Telegram API calls through curl."""
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
- resp = run_curl(request.method, real_url, data)
146
- return Response(resp, mimetype='application/json')
147
-
148
 
149
  def start_bridge():
150
- """Start Flask bridge in a daemon thread."""
151
- log.info(f"🌉 Starting curl bridge on 0.0.0.0:{Config.BRIDGE_PORT}")
152
- # Suppress Flask request logs in production
153
- wlog = logging.getLogger('werkzeug')
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(3)
161
- log.info("🌉 Bridge ready")
162
-
163
 
164
- # ╔═══════════════════════════════════════════════════════════════════╗
165
- # ║ 🤖 BOT INITIALIZATION ║
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(message: Message) -> bool:
200
- """In groups: only respond to replies to bot or @mentions."""
201
- if not is_group(message):
202
  return True
203
 
204
- # Reply to bot's message
205
- if message.reply_to_message and message.reply_to_message.from_user:
206
- if message.reply_to_message.from_user.id == bot.id:
207
- return True
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
- # Check entities for mention
216
- if message.entities:
217
- for e in message.entities:
218
  if e.type == "mention":
219
- mention = message.text[e.offset:e.offset + e.length].lower()
220
- if mention == f"@{bot_username}":
221
  return True
222
 
223
  return False
224
 
225
-
226
- # ╔═══════════════════════════════════════════════════════════════════╗
227
- # ║ 📋 KEYBOARDS ║
228
- # ╚═══════════════════════════════════════════════════════════════════╝
229
-
230
- def kb_main():
231
- rows = [
232
- [InlineKeyboardButton(text="💬 Chat", callback_data="agent:chat"),
233
- InlineKeyboardButton(text="💻 Code", callback_data="agent:code"),
234
- InlineKeyboardButton(text="🔬 Research", callback_data="agent:research")],
235
- [InlineKeyboardButton(text="📋 Task", callback_data="agent:task"),
236
- InlineKeyboardButton(text="🎨 Creative", callback_data="agent:creative"),
237
- InlineKeyboardButton(text="⚙️ System", callback_data="agent:system")],
238
- [InlineKeyboardButton(text="🔄 Model", callback_data="nav:models"),
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
- if os.path.exists(ss_path):
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 message.answer(f"❌ Error: {str(e)[:300]}")
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 &lt;msg&gt; - Send to all users
607
- /ban &lt;id&gt; / /unban &lt;id&gt;
608
- /premium &lt;id&gt; - Toggle premium
609
- /shell &lt;cmd&gt; - Run shell command
610
- /sysinfo - System details
611
- /health - Health check
612
- /logs - Recent tool logs
613
- /restart - Restart bot
614
- /addbutton &lt;label&gt;|&lt;url&gt; - 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 &lt;query&gt; - Web search
629
- /code &lt;code or task&gt; - Code execution
630
- /image &lt;desc&gt; - Generate image (DALL-E)
631
- /summarize &lt;text/url&gt; - Summarize
632
- /translate &lt;lang&gt; &lt;text&gt; - Translate
633
- /tts &lt;text&gt; - Text to speech
634
-
635
- <b>📁 Files:</b>
636
- /read &lt;path&gt; - Read file
637
- /write &lt;path&gt; &lt;content&gt; - Write file
638
- /ls &lt;path&gt; - List files
639
-
640
- <b>🧠 Memory:</b>
641
- /memory - Memory panel
642
- /remember &lt;fact&gt; - Store memory
643
- /recall &lt;query&gt; - Search memory
644
-
645
- <b>🤖 Advanced:</b>
646
- /newbot &lt;token&gt; - Spawn new bot
647
- /bots - List spawned bots
648
- /self &lt;instruction&gt; - Self-modify
649
- /workflow - Workflows
650
- /persona &lt;name&gt; &lt;prompt&gt; - 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 &lt;msg&gt; - 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 &lt;query&gt;", 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 &lt;code or description&gt;", 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 &lt;description&gt;", 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 &lt;text or URL&gt;", 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 &lt;language&gt; &lt;text&gt;", 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 &lt;text&gt;", 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 &lt;path&gt;", 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 &lt;path&gt; &lt;content&gt;", 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
- @dp.message(Command("memory"))
794
- async def cmd_memory(message: Message):
795
- await message.answer("🧠 <b>Memory Management</b>", parse_mode=ParseMode.HTML, reply_markup=kb_memory())
796
 
 
 
 
797
 
798
- @dp.message(Command("remember"))
799
- async def cmd_remember(message: Message):
800
- f = message.text.split(maxsplit=1)
801
- if len(f) < 2: return await message.answer("Usage: /remember &lt;fact&gt;", 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 &lt;query&gt;", 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
- f"📊 <b>Your Statistics</b>\n\n"
822
- f"💬 Messages: <b>{user['total_messages']}</b>\n"
823
- f"🔤 Tokens: <b>{user['total_tokens']:,}</b>\n"
824
- f"📅 Joined: <b>{user['created_at']}</b>\n"
825
- f" Active: <b>{user['last_active']}</b>\n"
826
- f"🔄 Model: <b>{user['preferred_model']}</b>\n"
827
- f" Premium: <b>{'Yes' if user['is_premium'] else 'No'}</b>\n"
828
- f"👑 Admin: <b>{'Yes' if is_admin(uid) else 'No'}</b>",
829
- parse_mode=ParseMode.HTML)
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 &lt;message&gt;", 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 &lt;TOKEN&gt;\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 &lt;instruction&gt;\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 cmd_tools(message: Message):
901
- cats = {
902
- "🔍 Search": ["web_search","read_webpage"],
903
- "💻 Code": ["execute_python","run_shell","install_package"],
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("workflow"))
924
- async def cmd_workflow(message: Message):
925
- if not is_admin(message.from_user.id): return await message.answer("🚫 Admin only.")
926
- parts = message.text.split(maxsplit=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927
  if len(parts) < 2:
928
- rows = DB.execute("SELECT * FROM workflows WHERE user_id=?", (message.from_user.id,), fetch=True)
929
- if not rows: return await message.answer("No workflows. Ask AI to create one!")
930
- text = "📋 <b>Workflows:</b>\n"
931
- for r in rows:
932
- text += f"\n• <b>{r['name']}</b> (ID:{r['id']}, runs:{r['run_count']})"
933
- text += "\n\nRun: /workflow &lt;id&gt;"
934
- return await message.answer(text, parse_mode=ParseMode.HTML)
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("persona"))
944
- async def cmd_persona(message: Message):
 
 
 
 
 
 
945
  parts = message.text.split(maxsplit=2)
946
  if len(parts) < 3:
947
- return await message.answer("Usage: /persona &lt;name&gt; &lt;system prompt&gt;", parse_mode=ParseMode.HTML)
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 &lt;name&gt; &lt;prompt&gt;", 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 &lt;message&gt;", 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
- uid = int(message.text.split()[1])
1009
- DB.update_user(uid, is_banned=1)
1010
- await message.answer(f"🚫 User {uid} banned.")
1011
- except: await message.answer("Usage: /ban &lt;user_id&gt;", parse_mode=ParseMode.HTML)
1012
-
1013
-
1014
- @dp.message(Command("unban"))
1015
- async def cmd_unban(message: Message):
1016
- if not is_admin(message.from_user.id): return
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 &lt;user_id&gt;", 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 &lt;user_id&gt;", 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 &lt;command&gt;", 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
- # ║ 📨 MESSAGE HANDLERS ║
1096
- # ╚═══════════════════════════════════════════════════════════════════╝
1097
 
1098
  @dp.message(F.text & ~F.text.startswith("/"))
1099
- async def handle_text(message: Message):
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
- text = message.text
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
- @dp.message(F.voice | F.audio)
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
- voice = message.voice or message.audio
1142
- file = await bot.get_file(voice.file_id)
1143
- fdata = await bot.download_file(file.file_path)
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("👁️ Analyzing image...")
1170
  try:
1171
- photo = message.photo[-1] # Largest size
1172
- file = await bot.get_file(photo.file_id)
1173
- fdata = await bot.download_file(file.file_path)
1174
- photo_bytes = fdata.read() if hasattr(fdata, 'read') else bytes(fdata)
1175
- b64 = base64.b64encode(photo_bytes).decode()
1176
-
1177
- caption = message.caption or "Analyze this image in detail."
1178
-
1179
- try: await status.delete()
1180
- except: pass
1181
-
1182
- await process_and_reply(message, caption, attachments=[{"type": "image", "b64": b64}])
1183
-
1184
  except Exception as e:
1185
- log.error(f"Photo error: {e}")
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
- content = content_bytes.decode("utf-8", errors="replace")
1220
  except:
1221
- content = f"[Binary file: {fname}, {len(content_bytes)} bytes]"
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.contact)
1262
- async def handle_contact(message: Message):
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 &lt;name&gt; &lt;prompt&gt;",
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
- # ║ 🔎 INLINE QUERY ║
1490
- # ╚═══════════════════════════════════════════════════════════════════╝
 
 
 
1491
 
1492
- @dp.inline_query()
1493
- async def inline_handler(inline_query: InlineQuery):
1494
- query = inline_query.query
1495
- if not query or len(query) < 3: return
1496
 
1497
  try:
1498
- r = await llm.chat([
1499
- {"role": "system", "content": "Answer in 2-3 sentences. Be helpful and concise."},
1500
- {"role": "user", "content": query}
1501
- ], model="gpt-4o-mini", max_tokens=200)
1502
-
1503
- text = r["content"]
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
- async def on_startup():
1536
- log.info("🤖 AgentForge v2 starting...")
 
1537
 
1538
- # Set bot commands
 
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" Bot: @{Config.BOT_USERNAME}")
1564
  except Exception as e:
1565
- log.warning(f"Commands/username warning: {e}")
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
- # Notify admins
1582
- for admin_id in Config.ADMIN_IDS:
1583
- try:
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 Exception as e:
1615
- log.warning(f"Webhook warning: {e}")
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
- try:
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())