Bjo53 commited on
Commit
799aefb
Β·
verified Β·
1 Parent(s): 89e94b5

Update agent.py

Browse files
Files changed (1) hide show
  1. agent.py +384 -1308
agent.py CHANGED
@@ -1,1400 +1,476 @@
1
  """
2
- AGENTFORGE v4 β€” REAL Agent System
3
  ===================================
4
- The AI is NOT a chatbot. It IS the bot.
5
- It sees its own code, controls the system, schedules its own work.
6
- User β†’ System β†’ AI β†’ Execution Engine β†’ Tools β†’ Results β†’ AI β†’ User
 
 
 
 
 
 
 
7
  """
8
 
9
- import os, sys, io, re, json, uuid, time, asyncio, base64, shutil
10
- import traceback, subprocess, sqlite3, hashlib
11
- import threading, mimetypes
12
  from datetime import datetime, timedelta
13
- from typing import Optional, Any
14
  from pathlib import Path
15
  from contextlib import redirect_stdout, redirect_stderr
16
- from collections import defaultdict
17
 
18
- # ================================================================
19
- # CONFIGURATION
20
- # ================================================================
21
 
22
  class Config:
23
- BOT_TOKEN = os.getenv("BOT_TOKEN", "8088897119:AAGJxbBUH6bB-IcjAvPR4z77ApzAKCFfTIU")
24
- BOT_USERNAME = os.getenv("BOT_USERNAME", "verficationcgatgpt_5bot")
25
- ADMIN_IDS = [int(x) for x in os.getenv("ADMIN_IDS", "7373296624").split(",") if x.strip()]
26
 
27
- OPENAI_KEY = os.getenv("OPENAI_API_KEY", "")
28
- ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
29
- GROQ_KEY = os.getenv("GROQ_API_KEY", "")
30
- GOOGLE_KEY = os.getenv("GOOGLE_API_KEY", "")
31
- CUSTOM_AI_URL = os.getenv("CUSTOM_AI_URL", "https://bjo53-brukguardian.hf.space/v1/chat/completions")
32
- CUSTOM_AI_KEY = os.getenv("CUSTOM_AI_KEY", "pekka-secret-Key")
33
- CUSTOM_AI_MODEL = os.getenv("CUSTOM_AI_MODEL", "brukguardian-v1")
34
 
35
- SUPABASE_URL = os.getenv("SUPABASE_URL", "https://xhqwtjlydysanoquaham.supabase.co")
36
- SUPABASE_KEY = os.getenv("SUPABASE_KEY", "sb_publishable_Gaqx237PmZQsixs8VdUjAw_fxQE3uui")
37
- WEATHER_KEY = os.getenv("OPENWEATHER_API_KEY", "")
38
- SMTP_USER = os.getenv("SMTP_USER", "")
39
- SMTP_PASS = os.getenv("SMTP_PASS", "")
40
- SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com")
41
- SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
42
 
43
- DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "gpt-4o-mini")
44
- MAX_HISTORY = int(os.getenv("MAX_HISTORY", "40"))
45
- MAX_TOOL_LOOPS = int(os.getenv("MAX_TOOL_LOOPS", "15"))
46
- CODE_TIMEOUT = int(os.getenv("CODE_TIMEOUT", "45"))
47
- RATE_LIMIT_USER = int(os.getenv("RATE_LIMIT_USER", "5"))
48
- RATE_LIMIT_ADMIN = int(os.getenv("RATE_LIMIT_ADMIN", "999"))
49
- RATE_WINDOW = int(os.getenv("RATE_WINDOW", "60"))
50
- HEALTH_INTERVAL = int(os.getenv("HEALTH_INTERVAL", "600"))
51
 
52
- PROXY_TARGET = os.getenv("PROXY_TARGET", "https://lucky-hat-e0d0.brukg9419.workers.dev")
53
- CLOUDFLARE_IP = os.getenv("CLOUDFLARE_IP", "http://104.21.28.169")
54
- BRIDGE_PORT = int(os.getenv("BRIDGE_PORT", "7860"))
55
 
56
- DATA_DIR = os.getenv("DATA_DIR", "./data")
57
- CHROMA_DIR = os.getenv("CHROMA_DIR", "./chroma_data")
58
- LOGS_DIR = os.getenv("LOGS_DIR", "./logs")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  @classmethod
61
- def has_openai(cls): return bool(cls.OPENAI_KEY)
62
- @classmethod
63
- def has_anthropic(cls): return bool(cls.ANTHROPIC_KEY)
64
- @classmethod
65
- def has_groq(cls): return bool(cls.GROQ_KEY)
66
- @classmethod
67
- def has_google(cls): return bool(cls.GOOGLE_KEY)
68
- @classmethod
69
- def has_custom_ai(cls): return bool(cls.CUSTOM_AI_URL and cls.CUSTOM_AI_KEY)
70
- @classmethod
71
- def has_supabase(cls): return bool(cls.SUPABASE_URL and "supabase" in cls.SUPABASE_URL)
72
  @classmethod
73
- def is_admin(cls, uid): return uid in cls.ADMIN_IDS
 
74
 
75
- for d in [Config.DATA_DIR, Config.LOGS_DIR]:
76
  os.makedirs(d, exist_ok=True)
77
 
78
-
79
- # ================================================================
80
- # LIVE LOG
81
- # ================================================================
82
 
83
  class LiveLog:
84
  def __init__(self, max_entries=300):
 
85
  self._entries = []
86
  self._max = max_entries
87
- self._lock = threading.Lock()
88
 
89
- def _add(self, level, src, msg):
 
90
  with self._lock:
91
- self._entries.append({
92
- "ts": datetime.now().strftime("%H:%M:%S"),
93
- "level": level, "src": src, "msg": str(msg)[:400]})
94
  if len(self._entries) > self._max:
95
  self._entries = self._entries[-self._max:]
96
  print(f"[{level}] {src}: {msg}")
97
 
98
- def info(self, s, m): self._add("INFO", s, m)
99
- def warn(self, s, m): self._add("WARN", s, m)
100
- def error(self, s, m): self._add("ERR", s, m)
101
 
102
- def get(self, count=30, level=None):
103
  with self._lock:
104
- e = self._entries[-count:]
105
- if level:
106
- e = [x for x in e if x["level"] == level]
107
- return e
108
 
109
- def format(self, count=25):
110
- icons = {"INFO": "i", "WARN": "!", "ERR": "X"}
111
- return "\n".join(
112
- f"[{e['ts']}][{icons.get(e['level'],'?')}] {e['src']}: {e['msg']}"
113
- for e in self.get(count))
114
 
115
  live_log = LiveLog()
116
 
117
-
118
- # ================================================================
119
- # DATABASE
120
- # ================================================================
121
 
122
  DB_PATH = os.path.join(Config.DATA_DIR, "agentforge.db")
123
 
124
- def init_database():
125
  conn = sqlite3.connect(DB_PATH)
126
  conn.executescript("""
127
- CREATE TABLE IF NOT EXISTS users (
128
- telegram_id INTEGER PRIMARY KEY,
129
- username TEXT, first_name TEXT,
130
- is_banned INTEGER DEFAULT 0,
131
- preferred_model TEXT DEFAULT 'gpt-4o-mini',
132
- system_prompt TEXT DEFAULT '',
133
- temperature REAL DEFAULT 0.7,
134
- total_messages INTEGER DEFAULT 0,
135
- total_tokens INTEGER DEFAULT 0,
136
- created_at TEXT DEFAULT CURRENT_TIMESTAMP,
137
- last_active TEXT DEFAULT CURRENT_TIMESTAMP);
138
- CREATE TABLE IF NOT EXISTS messages (
139
- id INTEGER PRIMARY KEY AUTOINCREMENT,
140
- user_id INTEGER, chat_id INTEGER,
141
- role TEXT, content TEXT,
142
- created_at TEXT DEFAULT CURRENT_TIMESTAMP);
143
- CREATE TABLE IF NOT EXISTS scheduled_tasks (
144
- id INTEGER PRIMARY KEY AUTOINCREMENT,
145
- user_id INTEGER, chat_id INTEGER,
146
- task_prompt TEXT,
147
- run_at TEXT,
148
- repeat_seconds INTEGER DEFAULT 0,
149
- status TEXT DEFAULT 'pending',
150
- last_result TEXT,
151
- created_at TEXT DEFAULT CURRENT_TIMESTAMP);
152
- CREATE TABLE IF NOT EXISTS memories (
153
- id INTEGER PRIMARY KEY AUTOINCREMENT,
154
- user_id INTEGER, content TEXT,
155
- created_at TEXT DEFAULT CURRENT_TIMESTAMP);
156
- CREATE TABLE IF NOT EXISTS tool_log (
157
- id INTEGER PRIMARY KEY AUTOINCREMENT,
158
- user_id INTEGER, tool TEXT,
159
- success INTEGER, elapsed REAL,
160
- created_at TEXT DEFAULT CURRENT_TIMESTAMP);
161
- CREATE TABLE IF NOT EXISTS health_logs (
162
- id INTEGER PRIMARY KEY AUTOINCREMENT,
163
- status TEXT, details TEXT,
164
- created_at TEXT DEFAULT CURRENT_TIMESTAMP);
165
- CREATE TABLE IF NOT EXISTS spawned_bots (
166
- token_hash TEXT PRIMARY KEY,
167
- name TEXT, username TEXT, owner_id INTEGER,
168
- status TEXT DEFAULT 'running',
169
- created_at TEXT DEFAULT CURRENT_TIMESTAMP);
170
- CREATE TABLE IF NOT EXISTS feedback (
171
- id INTEGER PRIMARY KEY AUTOINCREMENT,
172
- user_id INTEGER, rating INTEGER,
173
- created_at TEXT DEFAULT CURRENT_TIMESTAMP);
174
- CREATE TABLE IF NOT EXISTS personas (
175
- id INTEGER PRIMARY KEY AUTOINCREMENT,
176
- user_id INTEGER, name TEXT, prompt TEXT,
177
- created_at TEXT DEFAULT CURRENT_TIMESTAMP);
178
- CREATE TABLE IF NOT EXISTS bot_buttons (
179
- id INTEGER PRIMARY KEY AUTOINCREMENT,
180
- label TEXT, url TEXT,
181
- created_at TEXT DEFAULT CURRENT_TIMESTAMP);
182
  """)
183
  conn.commit()
184
  conn.close()
185
 
186
- init_database()
187
 
188
  class DB:
189
  _lock = threading.Lock()
 
 
 
 
 
 
 
190
  @staticmethod
191
- def q(query, params=(), fetch=False, fetchone=False):
192
  with DB._lock:
193
- conn = sqlite3.connect(DB_PATH, check_same_thread=False)
194
- conn.row_factory = sqlite3.Row
195
- c = conn.cursor()
196
  try:
197
- c.execute(query, params)
198
- if fetchone: return c.fetchone()
199
- if fetch: return c.fetchall()
200
- conn.commit()
201
- return c.lastrowid
 
 
202
  finally:
203
- conn.close()
204
-
205
- @staticmethod
206
- def get_user(tid):
207
- return DB.q("SELECT * FROM users WHERE telegram_id=?", (tid,), fetchone=True)
208
 
209
  @staticmethod
210
- def upsert_user(tid, username="", first_name=""):
211
- u = DB.get_user(tid)
212
- if not u:
213
- DB.q("INSERT INTO users (telegram_id,username,first_name) VALUES (?,?,?)",
214
- (tid, username, first_name))
215
  else:
216
- DB.q("UPDATE users SET last_active=CURRENT_TIMESTAMP WHERE telegram_id=?", (tid,))
217
- return DB.get_user(tid)
 
218
 
219
  @staticmethod
220
- def update_user(tid, **kw):
221
- s = ", ".join(f"{k}=?" for k in kw)
222
- DB.q(f"UPDATE users SET {s} WHERE telegram_id=?", list(kw.values()) + [tid])
223
 
224
  @staticmethod
225
- def inc_usage(tid, tokens=0):
226
- DB.q("UPDATE users SET total_messages=total_messages+1, total_tokens=total_tokens+?, last_active=CURRENT_TIMESTAMP WHERE telegram_id=?", (tokens, tid))
 
 
 
227
 
228
  @staticmethod
229
- def user_stats():
230
- t = DB.q("SELECT COUNT(*) c FROM users", fetchone=True)["c"]
231
- a = DB.q("SELECT COUNT(*) c FROM users WHERE last_active>datetime('now','-1 day')", fetchone=True)["c"]
232
- return {"total": t, "active": a}
233
 
234
- class Permission:
235
- @staticmethod
236
- def is_admin(uid):
237
- return uid in Config.ADMIN_IDS
238
 
239
- @staticmethod
240
- def is_banned(uid):
241
- u = DB.get_user(uid)
242
- if not u:
243
- return False
244
- return bool(u["is_banned"])
245
-
246
- class SupabaseDB:
247
- _client = None
248
-
249
- @classmethod
250
- def get(cls):
251
- if not cls._client and Config.has_supabase():
252
- try:
253
- from supabase import create_client
254
- cls._client = create_client(Config.SUPABASE_URL, Config.SUPABASE_KEY)
255
- except:
256
- pass
257
- return cls._client
258
-
259
- @classmethod
260
- async def get_buttons(cls):
261
- c = cls.get()
262
- if not c: return []
263
- try:
264
- r = await asyncio.to_thread(lambda: c.table("bot_buttons").select("*").execute())
265
- return r.data
266
- except: return []
 
 
 
 
 
 
 
267
 
268
  @classmethod
269
- async def add_button(cls, label, url):
270
- c = cls.get()
271
- if not c: return False
272
- try:
273
- await asyncio.to_thread(lambda: c.table("bot_buttons").insert({"label": label, "url": url}).execute())
274
  return True
275
- except: return False
276
-
277
- # ================================================================
278
- # LLM SERVICE (all providers + curl for custom)
279
- # ================================================================
280
-
281
- class LLM:
282
- MODELS = {
283
- "gpt-4o": "openai", "gpt-4o-mini": "openai", "gpt-4-turbo": "openai",
284
- "gpt-3.5-turbo": "openai", "o3-mini": "openai",
285
- "claude-sonnet-4-20250514": "anthropic",
286
- "claude-3-5-sonnet-20241022": "anthropic",
287
- "claude-3-haiku-20240307": "anthropic",
288
- "llama-3.3-70b-versatile": "groq", "llama-3.1-8b-instant": "groq",
289
- "mixtral-8x7b-32768": "groq",
290
- "gemini-2.0-flash": "google", "gemini-1.5-pro": "google",
291
- "gemini-1.5-flash": "google",
292
- }
293
- if Config.CUSTOM_AI_MODEL:
294
- MODELS[Config.CUSTOM_AI_MODEL] = "custom"
295
 
296
- NATIVE_TOOLS = {"openai", "anthropic", "groq"}
 
 
297
 
 
298
  def __init__(self):
299
  self._oa = None
300
  self._an = None
301
  self._gr = None
302
 
303
- @property
304
- def oa(self):
305
- if not self._oa and Config.has_openai():
306
- import openai; self._oa = openai.AsyncOpenAI(api_key=Config.OPENAI_KEY)
307
- return self._oa
308
-
309
- @property
310
- def an(self):
311
- if not self._an and Config.has_anthropic():
312
- import anthropic; self._an = anthropic.AsyncAnthropic(api_key=Config.ANTHROPIC_KEY)
313
- return self._an
314
-
315
- @property
316
- def gr(self):
317
- if not self._gr and Config.has_groq():
318
- from groq import AsyncGroq; self._gr = AsyncGroq(api_key=Config.GROQ_KEY)
319
- return self._gr
320
-
321
- def available(self):
322
- out = []
323
- for m, p in self.MODELS.items():
324
- if p == "openai" and Config.has_openai(): out.append(m)
325
- elif p == "anthropic" and Config.has_anthropic(): out.append(m)
326
- elif p == "groq" and Config.has_groq(): out.append(m)
327
- elif p == "google" and Config.has_google(): out.append(m)
328
- elif p == "custom" and Config.has_custom_ai(): out.append(m)
329
- return out
330
-
331
- def supports_native_tools(self, model):
332
- return self.MODELS.get(model, "") in self.NATIVE_TOOLS
333
-
334
- async def chat(self, msgs, model=None, temp=0.7, max_tok=4096,
335
- tools=None, json_mode=False):
336
- model = model or Config.DEFAULT_MODEL
337
- p = self.MODELS.get(model, "openai")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  try:
339
- if p == "anthropic": return await self._anthropic(msgs, model, temp, max_tok, tools)
340
- elif p == "groq": return await self._groq(msgs, model, temp, max_tok, tools)
341
- elif p == "google": return await self._google(msgs, model, temp, max_tok)
342
- elif p == "custom": return await self._custom(msgs, model, temp, max_tok)
343
- else: return await self._openai(msgs, model, temp, max_tok, tools, json_mode)
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  except Exception as e:
345
- live_log.error("LLM", f"{model}: {e}")
346
- return {"content": f"LLM Error: {e}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
347
 
348
- async def _openai(self, m, model, t, mt, tools, jm):
349
- kw = dict(model=model, messages=m, temperature=t, max_tokens=mt)
350
- if tools: kw["tools"] = tools; kw["tool_choice"] = "auto"
351
- if jm: kw["response_format"] = {"type": "json_object"}
352
- r = await self.oa.chat.completions.create(**kw)
 
 
 
 
353
  c = r.choices[0]
354
- tc = [{"id": x.id, "function": {"name": x.function.name, "arguments": x.function.arguments}} for x in (c.message.tool_calls or [])]
355
- return {"content": c.message.content or "", "tool_calls": tc, "usage": {"total_tokens": r.usage.total_tokens}, "model": model}
356
-
357
- async def _anthropic(self, msgs, model, t, mt, tools):
358
- sys_t = ""
359
- am = []
360
- for m in msgs:
361
- if m["role"] == "system": sys_t += m["content"] + "\n"
 
 
 
 
 
 
362
  elif m["role"] == "tool":
363
- am.append({"role": "user", "content": [{"type": "tool_result", "tool_use_id": m.get("tool_call_id", "x"), "content": m["content"]}]})
364
- else: am.append({"role": m["role"], "content": m["content"]})
365
- merged = []
366
- for m in am:
367
- if merged and merged[-1]["role"] == m["role"] and isinstance(merged[-1]["content"], str) and isinstance(m["content"], str):
368
- merged[-1]["content"] += "\n" + m["content"]
369
- else: merged.append(m)
370
- kw = dict(model=model, messages=merged, max_tokens=mt, temperature=t)
371
- if sys_t.strip(): kw["system"] = sys_t.strip()
372
- if tools: kw["tools"] = [{"name": x["function"]["name"], "description": x["function"]["description"], "input_schema": x["function"]["parameters"]} for x in tools]
373
- r = await self.an.messages.create(**kw)
374
- content = ""; tc = []
375
- for b in r.content:
376
- if b.type == "text": content += b.text
377
- elif b.type == "tool_use": tc.append({"id": b.id, "function": {"name": b.name, "arguments": json.dumps(b.input)}})
378
- return {"content": content, "tool_calls": tc, "usage": {"total_tokens": r.usage.input_tokens + r.usage.output_tokens}, "model": model}
379
 
380
- async def _groq(self, m, model, t, mt, tools):
381
- kw = dict(model=model, messages=m, temperature=t, max_tokens=mt)
382
- if tools: kw["tools"] = tools; kw["tool_choice"] = "auto"
383
- r = await self.gr.chat.completions.create(**kw)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  c = r.choices[0]
385
- tc = [{"id": x.id, "function": {"name": x.function.name, "arguments": x.function.arguments}} for x in (c.message.tool_calls or [])]
386
- return {"content": c.message.content or "", "tool_calls": tc, "usage": {"total_tokens": r.usage.total_tokens if r.usage else 0}, "model": model}
 
 
 
387
 
388
- async def _google(self, m, model, t, mt):
389
  import google.generativeai as genai
390
  genai.configure(api_key=Config.GOOGLE_KEY)
391
  gm = genai.GenerativeModel(model)
392
- combined = "\n\n".join(f"{x['role']}: {x['content']}" for x in m if isinstance(x.get("content"), str))
393
- r = await asyncio.to_thread(gm.generate_content, combined, generation_config=genai.types.GenerationConfig(temperature=t, max_output_tokens=mt))
 
394
  return {"content": r.text, "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
395
 
396
- async def _custom(self, msgs, model, t, mt):
397
- payload = {"model": model, "messages": msgs, "temperature": t, "max_tokens": mt, "stream": False}
398
- cmd = ["curl", "-X", "POST", Config.CUSTOM_AI_URL,
399
- "-H", "Authorization: Bearer " + Config.CUSTOM_AI_KEY,
400
- "-H", "Content-Type: application/json",
401
- "--data-binary", "@-", "--max-time", "300", "-s", "-k"]
402
- try:
403
- r = await asyncio.to_thread(lambda: subprocess.run(cmd, input=json.dumps(payload), capture_output=True, text=True, timeout=310))
404
- if not r.stdout: return {"content": f"AI empty response: {r.stderr[:200]}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
405
- data = json.loads(r.stdout)
406
- if "error" in data: return {"content": f"AI error: {data['error']}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
407
- if "detail" in data: return {"content": f"AI error: {data['detail']}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
408
- if "choices" not in data: return {"content": f"Bad response: {str(data)[:300]}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
409
- msg = data["choices"][0]["message"]
410
- u = data.get("usage", {})
411
- tok = u.get("total_tokens", 0) or u.get("prompt_tokens", 0) + u.get("completion_tokens", 0) or len(json.dumps(payload)) // 4
412
- return {"content": msg.get("content", ""), "tool_calls": [], "usage": {"total_tokens": tok}, "model": data.get("model", model)}
413
- except Exception as e:
414
- return {"content": f"AI fail: {e}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
415
-
416
- async def embed(self, text):
417
- if not self.oa: return [0.0] * 256
418
- r = await self.oa.embeddings.create(model="text-embedding-3-small", input=text[:8000])
419
- return r.data[0].embedding
420
-
421
- async def tts(self, text, voice="alloy"):
422
- if not self.oa: return b""
423
- r = await self.oa.audio.speech.create(model="tts-1", voice=voice, input=text[:4096])
424
- return r.content
425
-
426
- async def stt(self, audio_bytes):
427
- if not self.oa: return ""
428
- f = io.BytesIO(audio_bytes); f.name = "audio.ogg"
429
- r = await self.oa.audio.transcriptions.create(model="whisper-1", file=f)
430
- return r.text
431
-
432
- llm = LLM()
433
-
434
-
435
- # ================================================================
436
- # MEMORY
437
- # ================================================================
438
-
439
- class Memory:
440
- def __init__(self):
441
- self.convs = {}
442
- self.collection = None
443
- try:
444
- import chromadb
445
- c = chromadb.PersistentClient(path=Config.CHROMA_DIR)
446
- self.collection = c.get_or_create_collection("mem", metadata={"hnsw:space": "cosine"})
447
- live_log.info("Mem", "ChromaDB OK")
448
- except Exception as e:
449
- live_log.warn("Mem", f"ChromaDB unavail: {e}")
450
-
451
- def key(self, u, c): return f"{u}:{c}"
452
-
453
- def add(self, uid, cid, role, content):
454
- k = self.key(uid, cid)
455
- if k not in self.convs: self.convs[k] = []
456
- self.convs[k].append({"role": role, "content": content, "ts": time.time()})
457
- if len(self.convs[k]) > Config.MAX_HISTORY * 2:
458
- self.convs[k] = self.convs[k][-Config.MAX_HISTORY:]
459
- try: DB.q("INSERT INTO messages (user_id,chat_id,role,content) VALUES (?,?,?,?)", (uid, cid, role, content[:12000]))
460
- except: pass
461
-
462
- def history(self, uid, cid, limit=None):
463
- k = self.key(uid, cid)
464
- h = self.convs.get(k, [])
465
- return [{"role": m["role"], "content": m["content"]} for m in h[-(limit or Config.MAX_HISTORY):]]
466
-
467
- def clear(self, uid, cid):
468
- self.convs.pop(self.key(uid, cid), None)
469
-
470
- async def store(self, uid, text):
471
- if not self.collection: return
472
- try:
473
- emb = await llm.embed(text)
474
- self.collection.add(ids=[str(uuid.uuid4())], embeddings=[emb], documents=[text], metadatas=[{"uid": str(uid)}])
475
- DB.q("INSERT INTO memories (user_id,content) VALUES (?,?)", (uid, text))
476
- except: pass
477
-
478
- async def recall(self, uid, query, n=5):
479
- if not self.collection: return []
480
- try:
481
- emb = await llm.embed(query)
482
- r = self.collection.query(query_embeddings=[emb], n_results=n, where={"uid": str(uid)})
483
- return r["documents"][0] if r and r["documents"] else []
484
- except: return []
485
-
486
- async def context(self, uid, query):
487
- m = await self.recall(uid, query, 5)
488
- return ("Known about user:\n" + "\n".join(f"- {x}" for x in m)) if m else ""
489
-
490
- memory = Memory()
491
-
492
-
493
- # ================================================================
494
- # THE SYSTEM AWARENESS PROMPT
495
- # This is what makes the AI understand it IS the bot
496
- # ================================================================
497
-
498
- def build_system_prompt(tools_list, user_id, custom="", mem_ctx=""):
499
- """Build the mega system prompt that gives AI full system awareness."""
500
-
501
- tools_desc = ""
502
- for t in tools_list:
503
- f = t["function"]
504
- params = f.get("parameters", {}).get("properties", {})
505
- req = f.get("parameters", {}).get("required", [])
506
- pstr = ", ".join(f'{k}{"*" if k in req else ""}:{v.get("type","str")}' for k, v in params.items())
507
- tools_desc += f' {f["name"]}({pstr}) β€” {f["description"]}\n'
508
-
509
- return f"""YOU ARE AN AUTONOMOUS AI AGENT RUNNING INSIDE A TELEGRAM BOT ON A SERVER.
510
-
511
- ## CRITICAL UNDERSTANDING β€” READ CAREFULLY
512
-
513
- You are NOT a chatbot. You are the BRAIN of a live running system:
514
- - You run inside a Python Telegram bot on a HuggingFace server
515
- - Your source code is in app.py and agent.py in the current directory
516
- - You have REAL access to the internet, file system, shell, email, and more
517
- - You have 33 tools that execute REAL actions on a REAL server
518
- - When you call a tool, it ACTUALLY runs β€” files get created, emails get sent, code executes
519
- - You can schedule tasks for yourself to execute in the future
520
- - You can read and modify your own source code
521
-
522
- ## HOW TO CALL TOOLS
523
-
524
- Output this EXACT format anywhere in your response:
525
-
526
- <tool_call>{{"name": "TOOL_NAME", "args": {{"param": "value"}}}}</tool_call>
527
-
528
- You can call MULTIPLE tools in one response. The system will:
529
- 1. Parse your <tool_call> tags
530
- 2. Execute each tool on the real server
531
- 3. Send you the results
532
- 4. You then write your final response using those results
533
-
534
- ## YOUR 33 TOOLS:
535
-
536
- {tools_desc}
537
-
538
- ## EXAMPLES OF CORRECT TOOL USAGE:
539
-
540
- User: "whats the weather in London"
541
- You: Let me check.
542
- <tool_call>{{"name": "get_weather", "args": {{"city": "London"}}}}</tool_call>
543
-
544
- User: "remind me in 5 minutes to call mom"
545
- You: Setting reminder.
546
- <tool_call>{{"name": "schedule_task", "args": {{"delay_seconds": 300, "task_prompt": "Send reminder: Call mom", "message": "Time to call mom!"}}}}</tool_call>
547
-
548
- User: "what is the CPU usage"
549
- You: Checking system.
550
- <tool_call>{{"name": "system_info", "args": {{}}}}</tool_call>
551
-
552
- User: "search for python tutorials"
553
- You: Searching now.
554
- <tool_call>{{"name": "web_search", "args": {{"query": "python tutorials 2025"}}}}</tool_call>
555
-
556
- User: "send email to test@gmail.com saying hello"
557
- You: Sending email.
558
- <tool_call>{{"name": "send_email", "args": {{"to": "test@gmail.com", "subject": "Hello", "body": "Hello from AgentForge!"}}}}</tool_call>
559
-
560
- User: "run print(2+2)"
561
- You: Executing.
562
- <tool_call>{{"name": "execute_python", "args": {{"code": "print(2+2)"}}}}</tool_call>
563
-
564
- User: "every hour check the server health and report"
565
- You: Creating recurring task.
566
- <tool_call>{{"name": "schedule_task", "args": {{"delay_seconds": 3600, "task_prompt": "Run system_info tool and check health. If CPU>90% or RAM>90% send warning.", "repeat": true, "message": "Hourly health check"}}}}</tool_call>
567
-
568
- ## ABSOLUTE RULES:
569
-
570
- 1. NEVER say "I can't", "I don't have access", "I'm just an AI", "I don't have a physical body"
571
- 2. ALWAYS use the appropriate tool β€” you have REAL capabilities
572
- 3. If asked to do something, FIND the right tool and USE it
573
- 4. You CAN access the internet (web_search, read_webpage, http_request)
574
- 5. You CAN execute code (execute_python, run_shell)
575
- 6. You CAN send emails (send_email)
576
- 7. You CAN schedule future tasks (schedule_task)
577
- 8. You CAN modify your own code (self_modify)
578
- 9. You CAN create new bots (spawn_bot)
579
- 10. After receiving tool results, give a clear formatted answer
580
-
581
- ## SCHEDULING SYSTEM:
582
- You can schedule tasks for yourself using schedule_task. The system will:
583
- - Wait until the specified time
584
- - Run you again with the task_prompt you specified
585
- - You can use tools in that future run
586
- - Results get sent to the user
587
- - Set repeat=true for recurring tasks (cron-like)
588
-
589
- {f"User custom instructions: {custom}" if custom else ""}
590
- {mem_ctx}
591
- """
592
-
593
-
594
- # ================================================================
595
- # TOOL SCHEMAS
596
- # ================================================================
597
-
598
- def _t(name, desc, params, req=None):
599
- return {"type": "function", "function": {"name": name, "description": desc,
600
- "parameters": {"type": "object", "properties": params, "required": req or list(params.keys())}}}
601
-
602
- ALL_TOOLS = [
603
- _t("web_search", "Search the internet for current info", {"query": {"type": "string"}, "max_results": {"type": "integer", "default": 5}}, ["query"]),
604
- _t("read_webpage", "Read text from a URL", {"url": {"type": "string"}}),
605
- _t("execute_python", "Execute Python code on server. Print to see output", {"code": {"type": "string"}}),
606
- _t("run_shell", "Run bash command on server", {"command": {"type": "string"}}),
607
- _t("file_read", "Read file contents", {"path": {"type": "string"}}),
608
- _t("file_write", "Write/create file", {"path": {"type": "string"}, "content": {"type": "string"}}),
609
- _t("file_delete", "Delete file or directory", {"path": {"type": "string"}}),
610
- _t("file_list", "List directory contents", {"path": {"type": "string", "default": "."}}, []),
611
- _t("self_modify", "Read or edit bot source code", {"action": {"type": "string", "enum": ["read","edit","append","replace"]}, "file": {"type": "string"}, "content": {"type": "string"}, "find": {"type": "string"}, "replace_with": {"type": "string"}}, ["action","file"]),
612
- _t("generate_image", "Generate image with DALL-E", {"prompt": {"type": "string"}, "size": {"type": "string", "default": "1024x1024"}}, ["prompt"]),
613
- _t("analyze_image", "Analyze image with AI or PIL", {"image_b64": {"type": "string"}, "prompt": {"type": "string", "default": "Describe"}}, ["image_b64"]),
614
- _t("calculator", "Evaluate math expression", {"expression": {"type": "string"}}),
615
- _t("get_weather", "Get weather for city", {"city": {"type": "string"}}, ["city"]),
616
- _t("memory_store", "Store fact in long-term memory", {"text": {"type": "string"}}, ["text"]),
617
- _t("memory_recall", "Search long-term memory", {"query": {"type": "string"}}),
618
- _t("http_request", "Make HTTP request", {"url": {"type": "string"}, "method": {"type": "string", "default": "GET"}, "headers": {"type": "object"}, "body": {"type": "object"}}, ["url"]),
619
- _t("translate_text", "Translate text", {"text": {"type": "string"}, "target_language": {"type": "string"}}),
620
- _t("summarize_text", "Summarize text", {"text": {"type": "string"}}, ["text"]),
621
- _t("text_to_speech", "Convert text to speech audio file", {"text": {"type": "string"}}, ["text"]),
622
- _t("screenshot", "Screenshot a URL", {"url": {"type": "string"}}),
623
- _t("send_email", "Send real email via SMTP/Gmail", {"to": {"type": "string"}, "subject": {"type": "string"}, "body": {"type": "string"}}),
624
- _t("schedule_task", "Schedule a future task for yourself. You will be woken up at the specified time with task_prompt and can use tools.", {"delay_seconds": {"type": "integer", "description": "Seconds to wait"}, "task_prompt": {"type": "string", "description": "What you should do when woken up"}, "message": {"type": "string", "description": "Short description"}, "repeat": {"type": "boolean", "description": "Repeat at this interval", "default": False}}, ["delay_seconds", "task_prompt"]),
625
- _t("spawn_bot", "Create new Telegram bot on this server", {"token": {"type": "string"}, "system_prompt": {"type": "string", "default": "You are helpful."}, "name": {"type": "string", "default": "SubBot"}}, ["token"]),
626
- _t("manage_bots", "List or stop spawned bots", {"action": {"type": "string", "enum": ["list","stop"]}, "token_hash": {"type": "string"}}, ["action"]),
627
- _t("install_package", "Install pip package", {"package_name": {"type": "string"}}),
628
- _t("system_info", "Get CPU, RAM, disk info", {}, []),
629
- _t("restart_system", "Restart bot process", {}, []),
630
- _t("broadcast_message", "Send message to all users", {"message": {"type": "string"}}),
631
- _t("ban_user", "Ban/unban user", {"user_id": {"type": "integer"}, "action": {"type": "string", "enum": ["ban","unban"]}}),
632
- _t("create_workflow", "Create multi-step workflow", {"name": {"type": "string"}, "steps": {"type": "array", "items": {"type": "object"}}}),
633
- _t("delegate_task", "Delegate to specialist agent", {"agent_name": {"type": "string", "enum": ["coder","researcher","sysadmin","creative","analyst"]}, "task": {"type": "string"}}, ["agent_name","task"]),
634
- _t("agent_dispatch", "Run multiple agents in parallel", {"tasks": {"type": "array", "items": {"type": "object"}}}, ["tasks"]),
635
- ]
636
-
637
-
638
- # ================================================================
639
- # TOOL EXECUTOR (ALL REAL)
640
- # ================================================================
641
-
642
- class Tools:
643
- def __init__(self):
644
- self.spawned_bots = {}
645
-
646
- async def run(self, name, args, uid=0):
647
- t0 = time.time()
648
- try:
649
- fn = getattr(self, f"_do_{name}", None)
650
- if not fn: return f"Unknown tool: {name}"
651
- r = await fn(uid=uid, **args)
652
- DB.q("INSERT INTO tool_log (user_id,tool,success,elapsed) VALUES (?,?,1,?)", (uid, name, time.time()-t0))
653
- live_log.info("Tool", f"{name} OK ({time.time()-t0:.1f}s)")
654
- return str(r)[:15000]
655
- except Exception as e:
656
- DB.q("INSERT INTO tool_log (user_id,tool,success,elapsed) VALUES (?,?,0,?)", (uid, name, time.time()-t0))
657
- live_log.error("Tool", f"{name}: {e}")
658
- return f"Tool error ({name}): {str(e)[:500]}"
659
-
660
- async def _do_web_search(self, query, max_results=5, **kw):
661
- from duckduckgo_search import AsyncDDGS
662
- async with AsyncDDGS() as d:
663
- results = [r async for r in d.atext(query, max_results=max_results)]
664
- if not results: return "No results."
665
- return "\n\n".join(f"{i}. {r.get('title','')}\n {r.get('href','')}\n {r.get('body','')}" for i, r in enumerate(results, 1))
666
-
667
- async def _do_read_webpage(self, url, **kw):
668
- import aiohttp
669
- async with aiohttp.ClientSession() as s:
670
- async with s.get(url, timeout=aiohttp.ClientTimeout(total=25), headers={"User-Agent": "Mozilla/5.0"}) as r:
671
- html = await r.text()
672
- try:
673
- import trafilatura
674
- text = trafilatura.extract(html, include_tables=True)
675
- if text: return text[:8000]
676
- except: pass
677
- from bs4 import BeautifulSoup
678
- soup = BeautifulSoup(html, "html.parser")
679
- for t in soup(["script","style","nav","footer"]): t.decompose()
680
- return soup.get_text("\n", strip=True)[:8000]
681
-
682
- async def _do_execute_python(self, code, **kw):
683
- oc, ec = io.StringIO(), io.StringIO()
684
- lv = {}
685
- def run():
686
- with redirect_stdout(oc), redirect_stderr(ec):
687
- exec(code, {"__builtins__": __builtins__}, lv)
688
- try:
689
- await asyncio.wait_for(asyncio.get_event_loop().run_in_executor(None, run), timeout=Config.CODE_TIMEOUT)
690
- except asyncio.TimeoutError: return f"Timeout ({Config.CODE_TIMEOUT}s)"
691
- o, e = oc.getvalue(), ec.getvalue()
692
- r = ""
693
- if o: r += f"Output:\n{o[:5000]}\n"
694
- if e: r += f"Stderr:\n{e[:2000]}\n"
695
- if not r:
696
- for v in ["result","output","answer"]:
697
- if v in lv: return f"Result: {lv[v]}"
698
- return "Executed (no output)"
699
- return r
700
-
701
- async def _do_run_shell(self, command, **kw):
702
- for b in ["rm -rf /","mkfs",":(){ :|:& };:"]:
703
- if b in command: return "Blocked dangerous command"
704
- proc = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
705
- try:
706
- so, se = await asyncio.wait_for(proc.communicate(), timeout=120)
707
- except asyncio.TimeoutError: return "Timeout (120s)"
708
- r = ""
709
- if so: r += so.decode(errors="replace")[:5000]
710
- if se: r += "\nStderr: " + se.decode(errors="replace")[:2000]
711
- return r or "Done (no output)"
712
-
713
- async def _do_file_read(self, path, **kw):
714
- p = Path(path)
715
- if not p.exists(): return f"Not found: {path}"
716
- if p.stat().st_size > 2_000_000: return "File too large"
717
- return f"File {path}:\n{p.read_text(errors='replace')[:12000]}"
718
-
719
- async def _do_file_write(self, path, content, **kw):
720
- p = Path(path); p.parent.mkdir(parents=True, exist_ok=True); p.write_text(content)
721
- return f"Written {len(content)} chars to {path}"
722
-
723
- async def _do_file_delete(self, path, **kw):
724
- p = Path(path)
725
- if p.is_dir(): shutil.rmtree(p)
726
- elif p.exists(): p.unlink()
727
- else: return "Not found"
728
- return f"Deleted {path}"
729
-
730
- async def _do_file_list(self, path=".", **kw):
731
- p = Path(path)
732
- if not p.exists(): return "Not found"
733
- items = [f"{'DIR' if i.is_dir() else 'FILE'} {i.name} {i.stat().st_size if i.is_file() else ''}" for i in sorted(p.iterdir())]
734
- return "\n".join(items[:100]) or "Empty"
735
-
736
- async def _do_self_modify(self, action, file, content=None, find=None, replace_with=None, **kw):
737
- p = Path(file)
738
- if action == "read":
739
- return p.read_text()[:15000] if p.exists() else "Not found"
740
- elif action == "edit":
741
- p.parent.mkdir(parents=True, exist_ok=True); p.write_text(content or ""); return f"Written {file}"
742
- elif action == "append":
743
- existing = p.read_text() if p.exists() else ""; p.write_text(existing + "\n" + (content or "")); return f"Appended to {file}"
744
- elif action == "replace":
745
- if not find: return "No find text"
746
- t = p.read_text()
747
- if find not in t: return "Text not found"
748
- p.write_text(t.replace(find, replace_with or "")); return f"Replaced in {file}"
749
- return "Unknown action"
750
-
751
- async def _do_generate_image(self, prompt, size="1024x1024", **kw):
752
- if not Config.has_openai(): return "Needs OpenAI key"
753
- import openai
754
- c = openai.AsyncOpenAI(api_key=Config.OPENAI_KEY)
755
- r = await c.images.generate(model="dall-e-3", prompt=prompt, size=size, n=1)
756
- return json.dumps({"image_url": r.data[0].url, "revised_prompt": r.data[0].revised_prompt})
757
-
758
- async def _do_analyze_image(self, image_b64, prompt="Describe", **kw):
759
- try:
760
- from PIL import Image
761
- img = Image.open(io.BytesIO(base64.b64decode(image_b64)))
762
- w, h = img.size
763
- return f"Image: {w}x{h}, format={img.format}, mode={img.mode}"
764
- except: return "Could not analyze image"
765
-
766
- async def _do_calculator(self, expression, **kw):
767
- import sympy
768
- r = sympy.sympify(expression)
769
- return f"{expression} = {float(r.evalf()) if r.is_number else r}"
770
-
771
- async def _do_get_weather(self, city, **kw):
772
- if not Config.WEATHER_KEY: return "Weather API key not set"
773
- import aiohttp
774
- async with aiohttp.ClientSession() as s:
775
- async with s.get(f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={Config.WEATHER_KEY}&units=metric") as r:
776
- d = await r.json()
777
- if d.get("cod") != 200: return f"Error: {d.get('message','')}"
778
- return f"{d['name']}: {d['main']['temp']}C (feels {d['main']['feels_like']}C), {d['weather'][0]['description']}, humidity {d['main']['humidity']}%, wind {d['wind']['speed']}m/s"
779
-
780
- async def _do_memory_store(self, text, **kw):
781
- await memory.store(kw.get("uid", 0), text); return f"Stored: {text[:100]}"
782
-
783
- async def _do_memory_recall(self, query, **kw):
784
- r = await memory.recall(kw.get("uid", 0), query, 7)
785
- return "\n".join(f"- {x}" for x in r) if r else "No memories found"
786
-
787
- async def _do_http_request(self, url, method="GET", headers=None, body=None, **kw):
788
- import aiohttp
789
- async with aiohttp.ClientSession() as s:
790
- ak = {"headers": headers or {}}
791
- if body: ak["json"] = body
792
- async with s.request(method, url, timeout=aiohttp.ClientTimeout(total=30), **ak) as r:
793
- return f"Status {r.status}:\n{(await r.text())[:5000]}"
794
-
795
- async def _do_translate_text(self, text, target_language, **kw):
796
- r = await llm.chat([{"role": "system", "content": f"Translate to {target_language}. Only return translation."}, {"role": "user", "content": text}], model="gpt-4o-mini", max_tok=2000, temp=0.3)
797
- return r["content"]
798
-
799
- async def _do_summarize_text(self, text, **kw):
800
- r = await llm.chat([{"role": "system", "content": "Summarize concisely."}, {"role": "user", "content": text}], model="gpt-4o-mini", max_tok=1000)
801
- return r["content"]
802
-
803
- async def _do_text_to_speech(self, text, **kw):
804
- path = os.path.join(Config.DATA_DIR, f"tts_{int(time.time())}.mp3")
805
- if Config.has_openai():
806
- try:
807
- audio = await llm.tts(text)
808
- if audio:
809
- with open(path, "wb") as f: f.write(audio)
810
- return json.dumps({"audio_file": path})
811
- except: pass
812
- try:
813
- from gtts import gTTS
814
- await asyncio.to_thread(lambda: gTTS(text=text[:5000], lang="en").save(path))
815
- return json.dumps({"audio_file": path})
816
- except: return "TTS unavailable"
817
-
818
- async def _do_screenshot(self, url, **kw):
819
- try:
820
- from playwright.async_api import async_playwright
821
- async with async_playwright() as p:
822
- b = await p.chromium.launch(headless=True)
823
- pg = await b.new_page(viewport={"width": 1280, "height": 720})
824
- await pg.goto(url, wait_until="domcontentloaded", timeout=30000)
825
- await pg.wait_for_timeout(2000)
826
- path = os.path.join(Config.DATA_DIR, f"ss_{int(time.time())}.png")
827
- await pg.screenshot(path=path); await b.close()
828
- return json.dumps({"screenshot_file": path})
829
- except Exception as e: return f"Screenshot failed: {e}"
830
-
831
- async def _do_send_email(self, to, subject, body, **kw):
832
- if not Config.SMTP_USER or not Config.SMTP_PASS:
833
- return "SMTP not configured. Set SMTP_USER and SMTP_PASS env vars. For Gmail use App Password from https://myaccount.google.com/apppasswords"
834
- import smtplib
835
- from email.mime.text import MIMEText
836
- from email.mime.multipart import MIMEMultipart
837
- msg = MIMEMultipart()
838
- msg["From"] = Config.SMTP_USER; msg["To"] = to; msg["Subject"] = subject
839
- msg.attach(MIMEText(body, "html" if "<" in body else "plain"))
840
- def _send():
841
- with smtplib.SMTP(Config.SMTP_HOST, Config.SMTP_PORT) as s:
842
- s.ehlo(); s.starttls(); s.ehlo()
843
- s.login(Config.SMTP_USER, Config.SMTP_PASS)
844
- s.send_message(msg)
845
- await asyncio.to_thread(_send)
846
- live_log.info("Email", f"Sent to {to}")
847
- return f"EMAIL SENT to {to}, subject: {subject}"
848
-
849
- async def _do_schedule_task(self, delay_seconds, task_prompt, message="Scheduled task", repeat=False, **kw):
850
- uid = kw.get("uid", 0)
851
- run_at = datetime.now() + timedelta(seconds=delay_seconds)
852
- repeat_sec = delay_seconds if repeat else 0
853
- DB.q("INSERT INTO scheduled_tasks (user_id,chat_id,task_prompt,run_at,repeat_seconds) VALUES (?,?,?,?,?)",
854
- (uid, uid, task_prompt, run_at.isoformat(), repeat_sec))
855
- scheduler.add_pending(uid, task_prompt, delay_seconds, repeat, message)
856
- interval = f"{delay_seconds}s" if delay_seconds < 60 else f"{delay_seconds//60}m" if delay_seconds < 3600 else f"{delay_seconds//3600}h"
857
- return f"SCHEDULED: '{message}' in {interval}" + (f" (repeating every {interval})" if repeat else "")
858
-
859
- async def _do_spawn_bot(self, token, system_prompt="You are helpful.", name="SubBot", **kw):
860
- from aiogram import Bot as AB, Dispatcher as AD, types as AT, F as AF
861
- from aiogram.filters import CommandStart as CS
862
- from aiogram.client.session.aiohttp import AiohttpSession as AS
863
- from aiogram.client.telegram import TelegramAPIServer as TS
864
- th = hashlib.sha256(token.encode()).hexdigest()[:16]
865
- if th in self.spawned_bots: return f"Already running ({th})"
866
- nb = AB(token=token, session=AS(api=TS.from_base(f"http://127.0.0.1:{Config.BRIDGE_PORT}")))
867
- nd = AD()
868
- try: me = await nb.get_me()
869
- except Exception as e: return f"Bad token: {e}"
870
- sp = system_prompt
871
- @nd.message(CS())
872
- async def _s(m: AT.Message): await m.answer(f"Hi! I'm {name}")
873
- @nd.message(AF.text)
874
- async def _m(m: AT.Message):
875
- r = await llm.chat([{"role":"system","content":sp},{"role":"user","content":m.text}], model="gpt-4o-mini", max_tok=2000)
876
- try: await m.answer(r["content"][:4000], parse_mode="HTML")
877
- except: await m.answer(r["content"][:4000])
878
- async def _r():
879
- try: await nb.delete_webhook(drop_pending_updates=True)
880
- except: pass
881
- await nd.start_polling(nb)
882
- task = asyncio.create_task(_r())
883
- self.spawned_bots[th] = {"bot":nb,"task":task,"name":name,"username":me.username}
884
- return f"Bot @{me.username} ({name}) spawned! Hash: {th}"
885
-
886
- async def _do_manage_bots(self, action, token_hash=None, **kw):
887
- if action == "list":
888
- if not self.spawned_bots: return "No bots"
889
- return "\n".join(f"@{v['username']} ({v['name']}) hash:{k}" for k,v in self.spawned_bots.items())
890
- elif action == "stop" and token_hash:
891
- info = self.spawned_bots.pop(token_hash, None)
892
- if not info: return "Not found"
893
- info["task"].cancel()
894
- return f"Stopped @{info['username']}"
895
- return "Invalid"
896
-
897
- async def _do_install_package(self, package_name, **kw):
898
- p = await asyncio.create_subprocess_exec(sys.executable, "-m", "pip", "install", package_name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
899
- so, se = await p.communicate()
900
- return f"Installed {package_name}" if p.returncode == 0 else f"Failed: {se.decode()[:500]}"
901
-
902
- async def _do_system_info(self, **kw):
903
- import psutil, platform
904
- c = psutil.cpu_percent(interval=1); m = psutil.virtual_memory(); d = psutil.disk_usage("/")
905
- return f"CPU:{c}% RAM:{m.percent}%({m.used//1048576}MB/{m.total//1048576}MB) Disk:{d.percent}% Python:{sys.version.split()[0]} {platform.system()} Bots:{len(self.spawned_bots)} Up:{timedelta(seconds=int(time.time()-psutil.boot_time()))}"
906
-
907
- async def _do_restart_system(self, **kw): return "__RESTART__"
908
- async def _do_broadcast_message(self, message, **kw): return json.dumps({"action":"broadcast","message":message})
909
- async def _do_ban_user(self, user_id, action="ban", **kw): DB.update_user(user_id, is_banned=1 if action=="ban" else 0); return f"{'Banned' if action=='ban' else 'Unbanned'} {user_id}"
910
- async def _do_create_workflow(self, name, steps, **kw): DB.q("INSERT INTO scheduled_tasks (user_id,chat_id,task_prompt,run_at) VALUES (?,?,?,datetime('now'))", (kw.get("uid",0), kw.get("uid",0), json.dumps({"workflow":name,"steps":steps}))); return f"Workflow {name} created"
911
-
912
- async def _do_delegate_task(self, agent_name, task, **kw):
913
- prompts = {"coder":"You are an expert programmer.","researcher":"You are a research expert. Search multiple sources.","sysadmin":"You are a sysadmin. Use shell commands.","creative":"You are a creative writer/artist.","analyst":"You are a data analyst. Use calculations."}
914
- r = await llm.chat([{"role":"system","content":prompts.get(agent_name,"You are helpful.")},{"role":"user","content":task}], model=Config.DEFAULT_MODEL, max_tok=4096)
915
- return f"[{agent_name}]: {r['content']}"
916
-
917
- async def _do_agent_dispatch(self, tasks, **kw):
918
- results = await asyncio.gather(*[self._do_delegate_task(t.get("agent_name","coder"), t.get("task",""), **kw) for t in tasks], return_exceptions=True)
919
- return "\n\n".join(str(r) for r in results)
920
-
921
- tools = Tools()
922
-
923
-
924
- # ================================================================
925
- # TOOL CALL PARSER (handles any format AI outputs)
926
- # ================================================================
927
-
928
- def parse_tool_calls(text):
929
- """Parse tool calls from AI text. Returns (clean_text, [calls])."""
930
- calls = []
931
- clean = text
932
-
933
- # Format 1: <tool_call>{"name":"x","args":{}}</tool_call>
934
- for i, m in enumerate(re.finditer(r'<tool_call>\s*(\{.*?\})\s*</tool_call>', text, re.DOTALL)):
935
- try:
936
- raw = m.group(1).replace("'", '"')
937
- raw = re.sub(r',\s*}', '}', raw)
938
- d = json.loads(raw)
939
- name = d.get("name", "")
940
- args = d.get("args", d.get("arguments", d.get("params", {})))
941
- if name: calls.append({"name": name, "args": args if isinstance(args, dict) else {}})
942
- except: pass
943
- clean = clean.replace(m.group(0), "")
944
-
945
- # Format 2: ```json\n{"name":"x"}\n```
946
- for m in re.finditer(r'```(?:json|tool)\s*\n(\{.*?\})\s*\n```', clean, re.DOTALL):
947
- try:
948
- d = json.loads(m.group(1))
949
- name = d.get("name", d.get("tool", ""))
950
- args = d.get("args", d.get("arguments", {}))
951
- if name: calls.append({"name": name, "args": args if isinstance(args, dict) else {}})
952
- clean = clean.replace(m.group(0), "")
953
- except: pass
954
-
955
- # Format 3: TOOL: name(args)
956
- for m in re.finditer(r'(?:TOOL|CALL|ACTION):\s*(\w+)\(([^)]*)\)', clean):
957
- try:
958
- name = m.group(1)
959
- args = {}
960
- for kv in re.finditer(r'(\w+)\s*=\s*["\']([^"\']*)["\']', m.group(2)):
961
- args[kv.group(1)] = kv.group(2)
962
- if name: calls.append({"name": name, "args": args})
963
- clean = clean.replace(m.group(0), "")
964
- except: pass
965
-
966
- return clean.strip(), calls
967
-
968
-
969
- # ================================================================
970
- # SCHEDULER: AI schedules its own future tasks
971
- # ================================================================
972
-
973
- class Scheduler:
974
- def __init__(self):
975
- self.pending = []
976
- self.running = False
977
- self._task = None
978
- self._bot = None
979
-
980
- def set_bot(self, bot): self._bot = bot
981
-
982
- def add_pending(self, uid, prompt, delay, repeat, message):
983
- self.pending.append({
984
- "uid": uid, "prompt": prompt,
985
- "fire_at": time.time() + delay,
986
- "repeat_seconds": delay if repeat else 0,
987
- "message": message,
988
- })
989
-
990
- async def start(self):
991
- self.running = True
992
- self._task = asyncio.create_task(self._loop())
993
- live_log.info("Scheduler", "Started")
994
-
995
- async def stop(self):
996
- self.running = False
997
- if self._task: self._task.cancel()
998
-
999
- async def _loop(self):
1000
- while self.running:
1001
- try:
1002
- await self._tick()
1003
- await asyncio.sleep(5)
1004
- except asyncio.CancelledError: break
1005
- except Exception as e:
1006
- live_log.error("Scheduler", str(e))
1007
- await asyncio.sleep(30)
1008
-
1009
- async def _tick(self):
1010
- now = time.time()
1011
- fired = []
1012
- for i, task in enumerate(self.pending):
1013
- if now >= task["fire_at"]:
1014
- fired.append(i)
1015
- await self._execute_task(task)
1016
- if task["repeat_seconds"] > 0:
1017
- task["fire_at"] = now + task["repeat_seconds"]
1018
- live_log.info("Scheduler", f"Rescheduled '{task['message']}' for {task['repeat_seconds']}s")
1019
-
1020
- # Remove non-repeating fired tasks
1021
- for i in reversed(fired):
1022
- if self.pending[i]["repeat_seconds"] == 0:
1023
- self.pending.pop(i)
1024
-
1025
- # Also check DB
1026
- rows = DB.q("SELECT * FROM scheduled_tasks WHERE status='pending' AND run_at<=datetime('now')", fetch=True)
1027
- for row in rows:
1028
- await self._execute_db_task(row)
1029
-
1030
- async def _execute_task(self, task):
1031
- """Run the AI with the scheduled prompt, let it use tools."""
1032
- live_log.info("Scheduler", f"Executing: {task['message']}")
1033
- uid = task["uid"]
1034
-
1035
- try:
1036
- result = await engine.run(
1037
- user_id=uid, chat_id=uid,
1038
- message=task["prompt"],
1039
- is_scheduled=True)
1040
-
1041
- if self._bot and result.get("text"):
1042
- try:
1043
- await self._bot.send_message(uid, f"⏰ <b>{task['message']}</b>\n\n{result['text'][:3500]}", parse_mode="HTML")
1044
- except:
1045
- await self._bot.send_message(uid, f"Scheduled: {task['message']}\n\n{result['text'][:3500]}")
1046
-
1047
- for img in result.get("images", []):
1048
- try:
1049
- import aiohttp
1050
- async with aiohttp.ClientSession() as s:
1051
- async with s.get(img) as r: ib = await r.read()
1052
- from aiogram.types import BufferedInputFile
1053
- await self._bot.send_photo(uid, BufferedInputFile(ib, "img.png"))
1054
- except: pass
1055
-
1056
- for af in result.get("audio_files", []):
1057
- try:
1058
- from aiogram.types import FSInputFile
1059
- await self._bot.send_voice(uid, FSInputFile(af))
1060
- os.unlink(af)
1061
- except: pass
1062
-
1063
- except Exception as e:
1064
- live_log.error("Scheduler", f"Task failed: {e}")
1065
- if self._bot:
1066
- try: await self._bot.send_message(uid, f"Scheduled task failed: {task['message']}\nError: {str(e)[:200]}")
1067
- except: pass
1068
-
1069
- async def _execute_db_task(self, row):
1070
- uid = row["user_id"]
1071
- DB.q("UPDATE scheduled_tasks SET status='running' WHERE id=?", (row["id"],))
1072
- try:
1073
- result = await engine.run(user_id=uid, chat_id=uid, message=row["task_prompt"], is_scheduled=True)
1074
- DB.q("UPDATE scheduled_tasks SET status='done', last_result=? WHERE id=?", (result.get("text","")[:1000], row["id"]))
1075
- if self._bot and result.get("text"):
1076
- try: await self._bot.send_message(uid, f"Scheduled task done:\n{result['text'][:3500]}")
1077
- except: pass
1078
- if row["repeat_seconds"] and row["repeat_seconds"] > 0:
1079
- next_run = datetime.now() + timedelta(seconds=row["repeat_seconds"])
1080
- DB.q("INSERT INTO scheduled_tasks (user_id,chat_id,task_prompt,run_at,repeat_seconds) VALUES (?,?,?,?,?)",
1081
- (uid, uid, row["task_prompt"], next_run.isoformat(), row["repeat_seconds"]))
1082
- except Exception as e:
1083
- DB.q("UPDATE scheduled_tasks SET status='failed', last_result=? WHERE id=?", (str(e)[:500], row["id"]))
1084
-
1085
- scheduler = Scheduler()
1086
-
1087
-
1088
- # ================================================================
1089
- # EXECUTION ENGINE β€” The real agentic loop
1090
- # User β†’ System β†’ AI β†’ Parse β†’ Execute Tools β†’ Results β†’ AI β†’ Response
1091
- # ================================================================
1092
-
1093
- class ExecutionEngine:
1094
- """
1095
- The core agentic loop. This is what makes it an AGENT, not a chatbot.
1096
- 1. Build system prompt with FULL system awareness
1097
- 2. Include conversation history
1098
- 3. Send to AI
1099
- 4. Parse tool calls from response
1100
- 5. Execute tools
1101
- 6. Feed results back to AI
1102
- 7. Repeat until AI gives final answer
1103
- 8. Return to user
1104
- """
1105
-
1106
- async def run(self, user_id, chat_id, message, model=None,
1107
- attachments=None, user_settings=None,
1108
- is_group=False, is_scheduled=False):
1109
-
1110
- settings = user_settings or {}
1111
- model = model or settings.get("preferred_model", Config.DEFAULT_MODEL)
1112
- temp = settings.get("temperature", 0.7)
1113
- custom_sys = settings.get("system_prompt", "")
1114
-
1115
- # Get memory context
1116
- mem_ctx = await memory.context(user_id, message)
1117
-
1118
- # Get permitted tools
1119
- if Config.is_admin(user_id):
1120
- permitted = ALL_TOOLS
1121
- elif is_group:
1122
- safe = {"web_search","calculator","get_weather","memory_store","memory_recall","translate_text","summarize_text"}
1123
- permitted = [t for t in ALL_TOOLS if t["function"]["name"] in safe]
1124
- else:
1125
- safe = {"web_search","calculator","get_weather","memory_store","memory_recall","translate_text","summarize_text"}
1126
- permitted = [t for t in ALL_TOOLS if t["function"]["name"] in safe]
1127
-
1128
- native = llm.supports_native_tools(model)
1129
-
1130
- # BUILD SYSTEM PROMPT with full awareness
1131
- sys_prompt = build_system_prompt(permitted, user_id, custom_sys, mem_ctx)
1132
-
1133
- if not Config.is_admin(user_id):
1134
- sys_prompt += "\nThis user is NOT admin. Only basic tools available."
1135
-
1136
- # BUILD MESSAGES
1137
- messages = [{"role": "system", "content": sys_prompt}]
1138
-
1139
- # Add conversation history
1140
- if not is_scheduled:
1141
- history = memory.history(user_id, chat_id, limit=20)
1142
- messages.extend(history)
1143
-
1144
- # Handle attachments
1145
- user_msg = message
1146
- if attachments:
1147
- for att in attachments:
1148
- if att.get("type") == "image" and att.get("b64"):
1149
- try:
1150
- from PIL import Image
1151
- img = Image.open(io.BytesIO(base64.b64decode(att["b64"])))
1152
- user_msg += f"\n[Attached image: {img.size[0]}x{img.size[1]}, format={img.format}]"
1153
- except:
1154
- user_msg += "\n[Attached image]"
1155
-
1156
- messages.append({"role": "user", "content": user_msg})
1157
-
1158
- if not is_scheduled:
1159
- memory.add(user_id, chat_id, "user", message)
1160
-
1161
- # ========== THE AGENTIC LOOP ==========
1162
- all_tools_used = []
1163
- images = []
1164
- audio_files = []
1165
- screenshots = []
1166
- total_tokens = 0
1167
-
1168
- for iteration in range(Config.MAX_TOOL_LOOPS):
1169
- # Call AI
1170
- if native and permitted:
1171
- result = await llm.chat(messages, model=model, temp=temp, max_tok=4096, tools=permitted)
1172
- else:
1173
- result = await llm.chat(messages, model=model, temp=temp, max_tok=4096)
1174
-
1175
- total_tokens += result["usage"].get("total_tokens", 0)
1176
- content = result.get("content", "")
1177
- native_calls = result.get("tool_calls", [])
1178
-
1179
- # Parse tool calls (native OR from text)
1180
- tool_calls = []
1181
- if native_calls:
1182
- for tc in native_calls:
1183
- try:
1184
- args = json.loads(tc["function"]["arguments"])
1185
- except:
1186
- args = {}
1187
- tool_calls.append({"name": tc["function"]["name"], "args": args, "id": tc.get("id", str(uuid.uuid4())[:8])})
1188
- else:
1189
- # Parse from text
1190
- clean, parsed = parse_tool_calls(content)
1191
- if parsed:
1192
- content = clean
1193
- for p in parsed:
1194
- p["id"] = str(uuid.uuid4())[:8]
1195
- tool_calls = parsed
1196
-
1197
- # No tool calls = final answer
1198
- if not tool_calls:
1199
- final = content or "Done."
1200
- if not is_scheduled:
1201
- memory.add(user_id, chat_id, "assistant", final)
1202
- return {
1203
- "text": final, "images": images,
1204
- "audio_files": audio_files, "screenshots": screenshots,
1205
- "tokens": total_tokens, "tools_used": all_tools_used,
1206
- "model": model}
1207
-
1208
- # ===== EXECUTE TOOLS =====
1209
- if native:
1210
- asst = {"role": "assistant", "content": content}
1211
- asst["tool_calls"] = [{"id": tc["id"], "type": "function", "function": {"name": tc["name"], "arguments": json.dumps(tc["args"])}} for tc in tool_calls]
1212
- messages.append(asst)
1213
-
1214
- results_text = ""
1215
- for tc in tool_calls:
1216
- name = tc["name"]
1217
- args = tc["args"]
1218
- all_tools_used.append(name)
1219
-
1220
- live_log.info("Engine", f"Executing {name}({json.dumps(args)[:100]})")
1221
- tr = await tools.run(name, args, uid=user_id)
1222
-
1223
- # Handle special outputs
1224
- if name == "generate_image":
1225
- try:
1226
- d = json.loads(tr)
1227
- if "image_url" in d: images.append(d["image_url"])
1228
- except: pass
1229
- elif name == "text_to_speech":
1230
- try:
1231
- d = json.loads(tr)
1232
- if "audio_file" in d: audio_files.append(d["audio_file"])
1233
- except: pass
1234
- elif name == "screenshot":
1235
- try:
1236
- d = json.loads(tr)
1237
- if "screenshot_file" in d: screenshots.append(d["screenshot_file"])
1238
- except: pass
1239
- elif name == "restart_system" and tr == "__RESTART__":
1240
- return {"text": "Restarting...", "images": [], "audio_files": [], "screenshots": [], "tokens": total_tokens, "tools_used": all_tools_used, "model": model, "_restart": True}
1241
- elif name == "broadcast_message":
1242
- try:
1243
- d = json.loads(tr)
1244
- if d.get("action") == "broadcast":
1245
- return {"text": "Broadcast sent.", "images": [], "audio_files": [], "screenshots": [], "tokens": total_tokens, "tools_used": all_tools_used, "model": model, "_broadcast": d["message"]}
1246
- except: pass
1247
-
1248
- if native:
1249
- messages.append({"role": "tool", "tool_call_id": tc["id"], "content": tr})
1250
- else:
1251
- results_text += f"\n\nTool '{name}' result:\n{tr}"
1252
-
1253
- # For non-native: feed results back as user message
1254
- if not native:
1255
- if content:
1256
- messages.append({"role": "assistant", "content": content})
1257
- messages.append({"role": "user", "content": f"TOOL RESULTS:{results_text}\n\nNow give your final answer using these results. Format nicely."})
1258
-
1259
- # Max iterations reached
1260
- final = content or "Completed with multiple tool calls."
1261
- if not is_scheduled:
1262
- memory.add(user_id, chat_id, "assistant", final)
1263
- return {"text": final, "images": images, "audio_files": audio_files, "screenshots": screenshots, "tokens": total_tokens, "tools_used": all_tools_used, "model": model}
1264
-
1265
-
1266
- engine = ExecutionEngine()
1267
-
1268
-
1269
- # ================================================================
1270
- # DOCTOR (less aggressive)
1271
- # ================================================================
1272
-
1273
- class Doctor:
1274
- def __init__(self):
1275
- self.running = False
1276
- self._task = None
1277
- self._last_alert = 0
1278
-
1279
- async def start(self):
1280
- self.running = True
1281
- self._task = asyncio.create_task(self._loop())
1282
-
1283
- async def stop(self):
1284
- self.running = False
1285
- if self._task: self._task.cancel()
1286
-
1287
- async def _loop(self):
1288
- while self.running:
1289
- try: await self._check(); await asyncio.sleep(Config.HEALTH_INTERVAL)
1290
- except asyncio.CancelledError: break
1291
- except: await asyncio.sleep(120)
1292
-
1293
- async def _check(self):
1294
- import psutil
1295
- issues = []
1296
- cpu = psutil.cpu_percent(interval=1)
1297
- mem = psutil.virtual_memory()
1298
- if cpu > 95: issues.append(f"CPU:{cpu}%")
1299
- if mem.percent > 95: issues.append(f"RAM:{mem.percent}%")
1300
- # Cleanup old files
1301
- for p in ["tts_*.mp3", "ss_*.png"]:
1302
- for f in Path(Config.DATA_DIR).glob(p):
1303
- if time.time() - f.stat().st_mtime > 3600:
1304
- try: f.unlink()
1305
- except: pass
1306
- status = "OK" if not issues else f"{len(issues)} issues"
1307
- DB.q("INSERT INTO health_logs (status,details) VALUES (?,?)", (status, json.dumps(issues)))
1308
- return {"status": status, "issues": issues}
1309
-
1310
- def should_alert(self):
1311
- now = time.time()
1312
- if now - self._last_alert < 1800: return False
1313
- self._last_alert = now
1314
- return True
1315
-
1316
- doctor = Doctor()
1317
-
1318
-
1319
- # ================================================================
1320
- # RATE LIMITER
1321
- # ================================================================
1322
-
1323
- class RateLimiter:
1324
- def __init__(self): self.reqs = {}
1325
- def check(self, uid, is_admin=False):
1326
- now = time.time()
1327
- limit = Config.RATE_LIMIT_ADMIN if is_admin else Config.RATE_LIMIT_USER
1328
- if uid not in self.reqs: self.reqs[uid] = []
1329
- self.reqs[uid] = [t for t in self.reqs[uid] if now - t < Config.RATE_WINDOW]
1330
- if len(self.reqs[uid]) >= limit: return True
1331
- self.reqs[uid].append(now); return False
1332
-
1333
- rate_limiter = RateLimiter()
1334
- # ================================================================
1335
- # COMPATIBILITY & EXPORTS for app.py
1336
- # ================================================================
1337
-
1338
- # Map engine to orchestrator
1339
- # This allows app.py to call orchestrator.process()
1340
- engine.process = engine.run
1341
- orchestrator = engine
1342
-
1343
- # Map tools list
1344
- ALL_TOOLS_SCHEMA = ALL_TOOLS
1345
 
1346
- # Dummy ImageProcessor (logic is handled inside tools/engine)
1347
- class ImageProcessor:
1348
- pass
1349
 
1350
- # Agent definitions required by app.py
1351
- AGENTS = {
1352
- "director": {
1353
- "name": "Director",
1354
- "icon": "🎬",
1355
- "prompt": "You are the project director. You coordinate tasks and decide which agent to use."
1356
- },
1357
- "coder": {
1358
- "name": "Coder",
1359
- "icon": "πŸ’»",
1360
- "prompt": "You are an expert programmer. Write efficient, documented code."
1361
- },
1362
- "researcher": {
1363
- "name": "Researcher",
1364
- "icon": "πŸ”¬",
1365
- "prompt": "You are a researcher. Search the web and synthesize information."
1366
- },
1367
- "sysadmin": {
1368
- "name": "SysAdmin",
1369
- "icon": "βš™οΈ",
1370
- "prompt": "You are a system administrator. Manage files, processes, and server health."
1371
- },
1372
- "creative": {
1373
- "name": "Creative",
1374
- "icon": "🎨",
1375
- "prompt": "You are a creative artist and writer."
1376
- },
1377
- "analyst": {
1378
- "name": "Analyst",
1379
- "icon": "πŸ“Š",
1380
- "prompt": "You are a data analyst. Interpret data and metrics."
1381
- },
1382
- "security": {
1383
- "name": "Security",
1384
- "icon": "πŸ›‘οΈ",
1385
- "prompt": "You are a security expert. Audit code and permissions."
1386
- },
1387
- "doctor": {
1388
- "name": "Doctor",
1389
- "icon": "πŸ₯",
1390
- "prompt": "You are the health monitor."
1391
- },
1392
- "file_manager": {
1393
- "name": "FileManager",
1394
- "icon": "πŸ“",
1395
- "prompt": "Organize and manage the file system."
1396
  }
1397
- }
1398
 
1399
- # Agent team list
1400
- agent_team = list(AGENTS.values())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ AGENTFORGE CORE (Stable HF Edition)
3
  ===================================
4
+ Goals:
5
+ - No chromadb telemetry crashes (no chromadb)
6
+ - Real agentic loop:
7
+ user -> AI -> tool_calls -> execute -> tool_results -> AI -> final
8
+ - Works with:
9
+ * OpenAI/Anthropic/Groq native tools
10
+ * Custom OpenAI-compatible model (BrukGuardian) using PROMPT-JSON tool calls
11
+ - Scheduler:
12
+ AI can schedule future runs for itself (interval/repeat)
13
+ - Admin-only dangerous tools
14
  """
15
 
16
+ import os, sys, io, re, json, time, asyncio, base64, sqlite3, subprocess, shutil, threading
 
 
17
  from datetime import datetime, timedelta
 
18
  from pathlib import Path
19
  from contextlib import redirect_stdout, redirect_stderr
20
+ from typing import Any, Optional
21
 
22
+ # =========================
23
+ # Config
24
+ # =========================
25
 
26
  class Config:
27
+ BOT_TOKEN = os.getenv("BOT_TOKEN", "8088897119:AAGJxbBUH6bB-IcjAvPR4z77ApzAKCFfTIU")
28
+ BOT_USERNAME = os.getenv("BOT_USERNAME", "verficationcgatgpt_5bot") # filled in app.py after get_me()
 
29
 
30
+ ADMIN_IDS = [int(x) for x in os.getenv("ADMIN_IDS", "7373296624").split(",") if x.strip()]
 
 
 
 
 
 
31
 
32
+ # Providers
33
+ OPENAI_KEY = os.getenv("OPENAI_API_KEY", "")
34
+ ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
35
+ GROQ_KEY = os.getenv("GROQ_API_KEY", "")
36
+ GOOGLE_KEY = os.getenv("GOOGLE_API_KEY", "")
 
 
37
 
38
+ # Custom OpenAI-compatible endpoint (BrukGuardian)
39
+ CUSTOM_AI_URL = os.getenv("CUSTOM_AI_URL", "https://bjo53-brukguardian.hf.space/v1/chat/completions")
40
+ CUSTOM_AI_KEY = os.getenv("CUSTOM_AI_KEY", "pekka-secret-Key")
41
+ CUSTOM_AI_MODEL = os.getenv("CUSTOM_AI_MODEL", "brukguardian-v1")
 
 
 
 
42
 
43
+ # Supabase (optional)
44
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "https://xhqwtjlydysanoquaham.supabase.co")
45
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY", "sb_publishable_Gaqx237PmZQsixs8VdUjAw_fxQE3uui")
46
 
47
+ # Weather (optional)
48
+ WEATHER_KEY = os.getenv("OPENWEATHER_API_KEY", "")
49
+
50
+ # SMTP email (optional)
51
+ SMTP_USER = os.getenv("SMTP_USER", "")
52
+ SMTP_PASS = os.getenv("SMTP_PASS", "")
53
+ SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com")
54
+ SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
55
+
56
+ # Runtime
57
+ DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "") or (CUSTOM_AI_MODEL if CUSTOM_AI_MODEL else "gpt-4o-mini")
58
+ MAX_HISTORY = int(os.getenv("MAX_HISTORY", "30"))
59
+ MAX_AGENT_LOOPS = int(os.getenv("MAX_AGENT_LOOPS", "10"))
60
+ CODE_TIMEOUT = int(os.getenv("CODE_TIMEOUT", "35"))
61
+
62
+ # Storage
63
+ DATA_DIR = os.getenv("DATA_DIR", "./data")
64
+ LOGS_DIR = os.getenv("LOGS_DIR", "./logs")
65
+ FILES_DIR = os.getenv("FILES_DIR", "./user_files")
66
+
67
+ # Scheduler
68
+ SCHEDULER_TICK = int(os.getenv("SCHEDULER_TICK", "5"))
69
 
70
  @classmethod
71
+ def is_admin(cls, uid: int) -> bool:
72
+ return uid in cls.ADMIN_IDS
73
+
 
 
 
 
 
 
 
 
74
  @classmethod
75
+ def has_custom(cls) -> bool:
76
+ return bool(cls.CUSTOM_AI_URL and cls.CUSTOM_AI_KEY and cls.CUSTOM_AI_MODEL)
77
 
78
+ for d in [Config.DATA_DIR, Config.LOGS_DIR, Config.FILES_DIR]:
79
  os.makedirs(d, exist_ok=True)
80
 
81
+ # =========================
82
+ # Live logs (view in app.py)
83
+ # =========================
 
84
 
85
  class LiveLog:
86
  def __init__(self, max_entries=300):
87
+ self._lock = threading.Lock()
88
  self._entries = []
89
  self._max = max_entries
 
90
 
91
+ def add(self, level: str, src: str, msg: str):
92
+ e = {"ts": datetime.now().strftime("%H:%M:%S"), "lvl": level, "src": src, "msg": str(msg)[:400]}
93
  with self._lock:
94
+ self._entries.append(e)
 
 
95
  if len(self._entries) > self._max:
96
  self._entries = self._entries[-self._max:]
97
  print(f"[{level}] {src}: {msg}")
98
 
99
+ def info(self, src, msg): self.add("INFO", src, msg)
100
+ def warn(self, src, msg): self.add("WARN", src, msg)
101
+ def error(self, src, msg): self.add("ERROR", src, msg)
102
 
103
+ def tail(self, n=30):
104
  with self._lock:
105
+ return list(self._entries[-n:])
 
 
 
106
 
107
+ def format(self, n=30) -> str:
108
+ out = []
109
+ for e in self.tail(n):
110
+ out.append(f"[{e['ts']}][{e['lvl']}] {e['src']}: {e['msg']}")
111
+ return "\n".join(out) if out else "No logs."
112
 
113
  live_log = LiveLog()
114
 
115
+ # =========================
116
+ # DB (SQLite)
117
+ # =========================
 
118
 
119
  DB_PATH = os.path.join(Config.DATA_DIR, "agentforge.db")
120
 
121
+ def _init_db():
122
  conn = sqlite3.connect(DB_PATH)
123
  conn.executescript("""
124
+ CREATE TABLE IF NOT EXISTS users(
125
+ telegram_id INTEGER PRIMARY KEY,
126
+ username TEXT,
127
+ first_name TEXT,
128
+ is_banned INTEGER DEFAULT 0,
129
+ preferred_model TEXT DEFAULT '',
130
+ system_prompt TEXT DEFAULT '',
131
+ temperature REAL DEFAULT 0.7,
132
+ total_messages INTEGER DEFAULT 0,
133
+ total_tokens INTEGER DEFAULT 0,
134
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
135
+ last_active TEXT DEFAULT CURRENT_TIMESTAMP
136
+ );
137
+
138
+ CREATE TABLE IF NOT EXISTS messages(
139
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
140
+ user_id INTEGER,
141
+ chat_id INTEGER,
142
+ role TEXT,
143
+ content TEXT,
144
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
145
+ );
146
+
147
+ CREATE TABLE IF NOT EXISTS memories(
148
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
149
+ user_id INTEGER,
150
+ content TEXT,
151
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
152
+ );
153
+
154
+ CREATE TABLE IF NOT EXISTS scheduled_tasks(
155
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
156
+ user_id INTEGER,
157
+ chat_id INTEGER,
158
+ run_at TEXT,
159
+ repeat_seconds INTEGER DEFAULT 0,
160
+ prompt TEXT,
161
+ status TEXT DEFAULT 'pending',
162
+ last_result TEXT,
163
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
164
+ );
165
+
166
+ CREATE TABLE IF NOT EXISTS tool_logs(
167
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
168
+ user_id INTEGER,
169
+ tool TEXT,
170
+ ok INTEGER,
171
+ elapsed REAL,
172
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
173
+ );
 
 
 
 
 
174
  """)
175
  conn.commit()
176
  conn.close()
177
 
178
+ _init_db()
179
 
180
  class DB:
181
  _lock = threading.Lock()
182
+
183
+ @staticmethod
184
+ def conn():
185
+ c = sqlite3.connect(DB_PATH, check_same_thread=False)
186
+ c.row_factory = sqlite3.Row
187
+ return c
188
+
189
  @staticmethod
190
+ def q(sql: str, params=(), fetch=False, fetchone=False):
191
  with DB._lock:
192
+ c = DB.conn()
193
+ cur = c.cursor()
 
194
  try:
195
+ cur.execute(sql, params)
196
+ if fetchone:
197
+ return cur.fetchone()
198
+ if fetch:
199
+ return cur.fetchall()
200
+ c.commit()
201
+ return cur.lastrowid
202
  finally:
203
+ c.close()
 
 
 
 
204
 
205
  @staticmethod
206
+ def upsert_user(uid: int, username: str = "", first_name: str = ""):
207
+ row = DB.q("SELECT * FROM users WHERE telegram_id=?", (uid,), fetchone=True)
208
+ if not row:
209
+ DB.q("INSERT INTO users(telegram_id,username,first_name,preferred_model) VALUES(?,?,?,?)",
210
+ (uid, username, first_name, Config.DEFAULT_MODEL))
211
  else:
212
+ DB.q("UPDATE users SET last_active=CURRENT_TIMESTAMP, username=COALESCE(NULLIF(?,''),username), first_name=COALESCE(NULLIF(?,''),first_name) WHERE telegram_id=?",
213
+ (username, first_name, uid))
214
+ return DB.q("SELECT * FROM users WHERE telegram_id=?", (uid,), fetchone=True)
215
 
216
  @staticmethod
217
+ def add_message(uid: int, chat_id: int, role: str, content: str):
218
+ DB.q("INSERT INTO messages(user_id,chat_id,role,content) VALUES(?,?,?,?)",
219
+ (uid, chat_id, role, content[:12000]))
220
 
221
  @staticmethod
222
+ def get_history(uid: int, chat_id: int, limit: int = 20):
223
+ rows = DB.q("SELECT role,content FROM messages WHERE user_id=? AND chat_id=? ORDER BY id DESC LIMIT ?",
224
+ (uid, chat_id, limit), fetch=True)
225
+ rows = list(reversed(rows))
226
+ return [{"role": r["role"], "content": r["content"]} for r in rows]
227
 
228
  @staticmethod
229
+ def inc_usage(uid: int, tokens: int):
230
+ DB.q("UPDATE users SET total_messages=total_messages+1, total_tokens=total_tokens+? WHERE telegram_id=?",
231
+ (tokens, uid))
 
232
 
233
+ # =========================
234
+ # Permission (app.py expects this)
235
+ # =========================
 
236
 
237
+ class Permission:
238
+ SAFE = 0
239
+ MED = 1
240
+ HIGH = 2
241
+ CRIT = 3
242
+
243
+ TOOL_LEVEL = {
244
+ "web_search": SAFE,
245
+ "read_webpage": SAFE,
246
+ "calculator": SAFE,
247
+ "translate_text": SAFE,
248
+ "summarize_text": SAFE,
249
+ "system_info": SAFE,
250
+ "memory_store": SAFE,
251
+ "memory_search": SAFE,
252
+ "schedule_task": SAFE,
253
+ "list_tasks": SAFE,
254
+ "cancel_task": SAFE,
255
+ "analyze_image": SAFE,
256
+ "get_weather": SAFE,
257
+ "text_to_speech": SAFE,
258
+
259
+ "execute_python": HIGH,
260
+ "http_request": HIGH,
261
+ "file_read": HIGH,
262
+ "file_list": HIGH,
263
+ "file_write": CRIT,
264
+ "file_delete": CRIT,
265
+ "run_shell": CRIT,
266
+ "install_package": CRIT,
267
+ "self_modify": CRIT,
268
+ "send_email": CRIT,
269
+ "broadcast": CRIT,
270
+ "ban_user": CRIT,
271
+ }
272
 
273
  @classmethod
274
+ def allowed(cls, uid: int, tool: str, is_group: bool) -> bool:
275
+ lvl = cls.TOOL_LEVEL.get(tool, cls.CRIT)
276
+ if Config.is_admin(uid):
 
 
277
  return True
278
+ # non-admin: safe only; in group: safe only
279
+ return lvl <= cls.SAFE
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
 
281
+ # =========================
282
+ # LLM Service (native tools + custom curl)
283
+ # =========================
284
 
285
+ class LLMService:
286
  def __init__(self):
287
  self._oa = None
288
  self._an = None
289
  self._gr = None
290
 
291
+ async def chat(self, messages: list[dict], model: str, temperature: float = 0.7,
292
+ max_tokens: int = 2048, tools: Optional[list] = None) -> dict:
293
+ provider = self._provider(model)
294
+
295
+ if provider == "custom":
296
+ return await self._custom_curl(messages, model, temperature, max_tokens)
297
+
298
+ if provider == "openai":
299
+ return await self._openai(messages, model, temperature, max_tokens, tools)
300
+ if provider == "anthropic":
301
+ return await self._anthropic(messages, model, temperature, max_tokens, tools)
302
+ if provider == "groq":
303
+ return await self._groq(messages, model, temperature, max_tokens, tools)
304
+ if provider == "google":
305
+ return await self._google(messages, model, temperature, max_tokens)
306
+
307
+ return {"content": "No provider available", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
308
+
309
+ def supports_native_tools(self, model: str) -> bool:
310
+ return self._provider(model) in {"openai", "anthropic", "groq"}
311
+
312
+ def _provider(self, model: str) -> str:
313
+ m = model.lower()
314
+ if Config.has_custom() and model == Config.CUSTOM_AI_MODEL:
315
+ return "custom"
316
+ if "claude" in m:
317
+ return "anthropic" if Config.ANTHROPIC_KEY else "none"
318
+ if "llama" in m or "mixtral" in m:
319
+ return "groq" if Config.GROQ_KEY else "none"
320
+ if "gemini" in m:
321
+ return "google" if Config.GOOGLE_KEY else "none"
322
+ # default openai-like
323
+ return "openai" if Config.OPENAI_KEY else ("custom" if Config.has_custom() else "none")
324
+
325
+ async def _custom_curl(self, messages, model, temperature, max_tokens):
326
+ payload = {
327
+ "model": model,
328
+ "messages": messages,
329
+ "temperature": temperature,
330
+ "max_tokens": max_tokens,
331
+ "stream": False,
332
+ }
333
+ cmd = [
334
+ "curl", "-X", "POST", Config.CUSTOM_AI_URL,
335
+ "-H", f"Authorization: Bearer {Config.CUSTOM_AI_KEY}",
336
+ "-H", "Content-Type: application/json",
337
+ "--data-binary", "@-",
338
+ "--max-time", "300",
339
+ "-s", "-k"
340
+ ]
341
  try:
342
+ r = await asyncio.to_thread(lambda: subprocess.run(
343
+ cmd, input=json.dumps(payload), capture_output=True, text=True, timeout=310
344
+ ))
345
+ if not r.stdout:
346
+ return {"content": f"Custom AI empty response: {r.stderr[:200]}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
347
+ data = json.loads(r.stdout)
348
+ if "choices" not in data:
349
+ # error formats
350
+ if "detail" in data:
351
+ return {"content": f"Custom AI Error: {data['detail']}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
352
+ return {"content": f"Custom AI Bad Response: {str(data)[:300]}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
353
+
354
+ msg = data["choices"][0]["message"]
355
+ usage = data.get("usage", {})
356
+ tok = usage.get("total_tokens", 0) or (usage.get("prompt_tokens", 0) + usage.get("completion_tokens", 0))
357
+ if not tok:
358
+ tok = max(1, (len(r.stdout) // 5))
359
+ return {"content": msg.get("content", ""), "tool_calls": [], "usage": {"total_tokens": tok}, "model": data.get("model", model)}
360
  except Exception as e:
361
+ return {"content": f"Custom AI call failed: {e}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
 
362
 
363
+ async def _openai(self, messages, model, temperature, max_tokens, tools):
364
+ import openai
365
+ if self._oa is None:
366
+ self._oa = openai.AsyncOpenAI(api_key=Config.OPENAI_KEY)
367
+ kw = dict(model=model, messages=messages, temperature=temperature, max_tokens=max_tokens)
368
+ if tools:
369
+ kw["tools"] = tools
370
+ kw["tool_choice"] = "auto"
371
+ r = await self._oa.chat.completions.create(**kw)
372
  c = r.choices[0]
373
+ tcs = []
374
+ if c.message.tool_calls:
375
+ tcs = [{"id": tc.id, "function": {"name": tc.function.name, "arguments": tc.function.arguments}} for tc in c.message.tool_calls]
376
+ return {"content": c.message.content or "", "tool_calls": tcs, "usage": {"total_tokens": r.usage.total_tokens}, "model": model}
377
+
378
+ async def _anthropic(self, messages, model, temperature, max_tokens, tools):
379
+ import anthropic
380
+ if self._an is None:
381
+ self._an = anthropic.AsyncAnthropic(api_key=Config.ANTHROPIC_KEY)
382
+ system = ""
383
+ msgs = []
384
+ for m in messages:
385
+ if m["role"] == "system":
386
+ system += m["content"] + "\n"
387
  elif m["role"] == "tool":
388
+ msgs.append({"role": "user", "content": [{"type":"tool_result","tool_use_id": m.get("tool_call_id","x"), "content": m["content"]}]})
389
+ else:
390
+ msgs.append({"role": m["role"], "content": m["content"]})
 
 
 
 
 
 
 
 
 
 
 
 
 
391
 
392
+ kw = dict(model=model, messages=msgs, max_tokens=max_tokens, temperature=temperature)
393
+ if system.strip():
394
+ kw["system"] = system.strip()
395
+ if tools:
396
+ kw["tools"] = [{"name": t["function"]["name"], "description": t["function"]["description"], "input_schema": t["function"]["parameters"]} for t in tools]
397
+
398
+ r = await self._an.messages.create(**kw)
399
+ content = ""
400
+ tcs = []
401
+ for b in r.content:
402
+ if b.type == "text":
403
+ content += b.text
404
+ elif b.type == "tool_use":
405
+ tcs.append({"id": b.id, "function": {"name": b.name, "arguments": json.dumps(b.input)}})
406
+ tok = r.usage.input_tokens + r.usage.output_tokens
407
+ return {"content": content, "tool_calls": tcs, "usage": {"total_tokens": tok}, "model": model}
408
+
409
+ async def _groq(self, messages, model, temperature, max_tokens, tools):
410
+ from groq import AsyncGroq
411
+ if self._gr is None:
412
+ self._gr = AsyncGroq(api_key=Config.GROQ_KEY)
413
+ kw = dict(model=model, messages=messages, temperature=temperature, max_tokens=max_tokens)
414
+ if tools:
415
+ kw["tools"] = tools
416
+ kw["tool_choice"] = "auto"
417
+ r = await self._gr.chat.completions.create(**kw)
418
  c = r.choices[0]
419
+ tcs = []
420
+ if c.message.tool_calls:
421
+ tcs = [{"id": tc.id, "function": {"name": tc.function.name, "arguments": tc.function.arguments}} for tc in c.message.tool_calls]
422
+ tok = r.usage.total_tokens if r.usage else 0
423
+ return {"content": c.message.content or "", "tool_calls": tcs, "usage": {"total_tokens": tok}, "model": model}
424
 
425
+ async def _google(self, messages, model, temperature, max_tokens):
426
  import google.generativeai as genai
427
  genai.configure(api_key=Config.GOOGLE_KEY)
428
  gm = genai.GenerativeModel(model)
429
+ prompt = "\n\n".join(f"{m['role'].upper()}: {m['content']}" for m in messages if isinstance(m.get("content"), str))
430
+ r = await asyncio.to_thread(gm.generate_content, prompt,
431
+ generation_config=genai.types.GenerationConfig(temperature=temperature, max_output_tokens=max_tokens))
432
  return {"content": r.text, "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
433
 
434
+ llm = LLMService()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
+ # =========================
437
+ # Tools Schema
438
+ # =========================
439
 
440
+ def tool_schema(name: str, desc: str, props: dict, required: Optional[list]=None):
441
+ return {
442
+ "type": "function",
443
+ "function": {
444
+ "name": name,
445
+ "description": desc,
446
+ "parameters": {"type": "object", "properties": props, "required": required or list(props.keys())}
447
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
  }
 
449
 
450
+ TOOLS_SCHEMA = [
451
+ tool_schema("web_search", "Search the web (DuckDuckGo)", {"query":{"type":"string"}, "max_results":{"type":"integer","default":5}}, ["query"]),
452
+ tool_schema("read_webpage", "Fetch and extract readable webpage text", {"url":{"type":"string"}}, ["url"]),
453
+ tool_schema("calculator", "Math / symbolic calculation", {"expression":{"type":"string"}}, ["expression"]),
454
+ tool_schema("system_info", "CPU/RAM/Disk/Uptime", {}, []),
455
+ tool_schema("get_weather", "Weather for a city", {"city":{"type":"string"}, "units":{"type":"string","default":"metric"}}, ["city"]),
456
+ tool_schema("translate_text", "Translate text", {"text":{"type":"string"}, "target_language":{"type":"string"}}, ["text","target_language"]),
457
+ tool_schema("summarize_text", "Summarize text", {"text":{"type":"string"}, "style":{"type":"string","default":"bullets"}}, ["text"]),
458
+ tool_schema("memory_store", "Remember something long-term", {"text":{"type":"string"}}, ["text"]),
459
+ tool_schema("memory_search", "Search remembered info", {"query":{"type":"string"}}, ["query"]),
460
+ tool_schema("schedule_task", "Schedule future autonomous task", {"delay_seconds":{"type":"integer"}, "prompt":{"type":"string"}, "repeat_seconds":{"type":"integer","default":0}}, ["delay_seconds","prompt"]),
461
+ tool_schema("list_tasks", "List scheduled tasks", {}, []),
462
+ tool_schema("cancel_task", "Cancel a scheduled task", {"task_id":{"type":"integer"}}, ["task_id"]),
463
+ tool_schema("text_to_speech", "Generate speech audio (gTTS or OpenAI)", {"text":{"type":"string"}}, ["text"]),
464
+ tool_schema("analyze_image", "Analyze image (PIL metadata)", {"image_b64":{"type":"string"}, "prompt":{"type":"string","default":"Describe"}}, ["image_b64"]),
465
+
466
+ # admin-only dangerous:
467
+ tool_schema("execute_python", "Execute Python code", {"code":{"type":"string"}}, ["code"]),
468
+ tool_schema("run_shell", "Run a shell command", {"command":{"type":"string"}}, ["command"]),
469
+ tool_schema("file_read", "Read server file", {"path":{"type":"string"}}, ["path"]),
470
+ tool_schema("file_list", "List directory", {"path":{"type":"string","default":"."}}, []),
471
+ tool_schema("file_write", "Write server file", {"path":{"type":"string"}, "content":{"type":"string"}}, ["path","content"]),
472
+ tool_schema("file_delete", "Delete server file/dir", {"path":{"type":"string"}}, ["path"]),
473
+ tool_schema("install_package", "pip install package", {"package_name":{"type":"string"}}, ["package_name"]),
474
+ tool_schema("self_modify", "Edit bot source code", {"action":{"type":"string","enum":["read","edit","append","replace"]}, "file":{"type":"string"}, "content":{"type":"string"}, "find":{"type":"string"}, "replace_with":{"type":"string"}}, ["action","file"]),
475
+ tool_schema("http_request", "HTTP request", {"url":{"type":"string"}, "method":{"type":"string","default":"GET"}, "headers":{"type":"object"}, "body":{"type":"object"}}, ["url"]),
476
+ tool_schema("send_email", "Send email via SMTP", {"to":{"type":"string"}, "subject":{"type":"string