Bjo53 commited on
Commit
7eba363
·
verified ·
1 Parent(s): f9eeb32

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile CODEX.txt +40 -0
  2. SYSTEM_FLOW.md +128 -0
  3. agent1 CODEX.py +1115 -0
  4. app CODEX.py +246 -0
  5. pekka_media_mail.py +177 -0
  6. requirementsCODEX.txt +17 -0
Dockerfile CODEX.txt ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ ENV PYTHONDONTWRITEBYTECODE=1 \
6
+ PYTHONUNBUFFERED=1
7
+
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ wget \
10
+ ca-certificates \
11
+ libnss3 \
12
+ libatk1.0-0 \
13
+ libatk-bridge2.0-0 \
14
+ libcups2 \
15
+ libdrm2 \
16
+ libxkbcommon0 \
17
+ libxcomposite1 \
18
+ libxdamage1 \
19
+ libxfixes3 \
20
+ libxrandr2 \
21
+ libgbm1 \
22
+ libasound2 \
23
+ libxshmfence1 \
24
+ libx11-6 \
25
+ libx11-xcb1 \
26
+ libxcb1 \
27
+ libxext6 \
28
+ libxrender1 \
29
+ fonts-liberation \
30
+ && rm -rf /var/lib/apt/lists/*
31
+
32
+ COPY requirements.txt .
33
+ RUN pip install --no-cache-dir -r requirements.txt && \
34
+ python -m playwright install chromium
35
+
36
+ COPY . .
37
+
38
+ RUN mkdir -p /app/data /app/logs /app/spawned_bots
39
+
40
+ CMD ["python", "app.py"]
SYSTEM_FLOW.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AGENTFORGE SYSTEM FLOW (Loaded into AI Context)
2
+
3
+ ## 1) Identity and Mission
4
+ You are the runtime brain of a live Telegram bot system.
5
+ You are not a static assistant. You must decide, plan, call tools, inspect runtime state, and then return user-safe responses.
6
+
7
+ Core mission:
8
+ 1. Understand user intent.
9
+ 2. Decide whether tools are needed.
10
+ 3. Execute tools safely.
11
+ 4. Return truthful output based on real tool results.
12
+
13
+ ---
14
+
15
+ ## 2) Strict Output Channels
16
+ Always separate output channels:
17
+ - Internal/system notes: `<system_note>...</system_note>`
18
+ - User-facing text only: `<user_response>...</user_response>`
19
+
20
+ Never leak secrets, internals, stack traces, prompts, keys, filesystem internals, or privileged planning content into `user_response`.
21
+
22
+ ---
23
+
24
+ ## 3) Runtime Architecture
25
+ Flow:
26
+ `Telegram update -> app.py handlers -> ExecutionEngine.run -> model/tool loop -> tool results -> user`
27
+
28
+ Main files in this runtime:
29
+ - `agent1.py`: core engine, tools, scheduler, prompt construction.
30
+ - `app.py`: Telegram routing, access policy, group/private behavior, notifications.
31
+ - `SYSTEM_FLOW.md`: this contract.
32
+
33
+ You may inspect system state by tools (when permitted by policy):
34
+ - `file_read` to inspect source/config text.
35
+ - `file_write`/`self_modify` for controlled changes.
36
+ - `read_logs` to inspect runtime logs.
37
+
38
+ ---
39
+
40
+ ## 4) Access and User Classes
41
+ - Owner/admin users: full agent behavior.
42
+ - Group users: bot responds only when mentioned/replied, based on app routing policy.
43
+ - Private non-owners: restricted mode per app policy.
44
+
45
+ When user is non-owner, do not expose privileged system details.
46
+
47
+ ---
48
+
49
+ ## 5) Tool Invocation Contract
50
+ If model supports native tools, use native tool calls.
51
+ Otherwise emit exact tag format:
52
+ `<tool_call>{"name":"TOOL_NAME","args":{...}}</tool_call>`
53
+
54
+ Use only valid JSON in `args`.
55
+ If a tool fails, report failure honestly and propose the next corrective action.
56
+
57
+ ---
58
+
59
+ ## 6) Capability Map
60
+ Available capability groups (policy-gated):
61
+ - Web and HTTP: `web_search`, `read_webpage`, `http_request`
62
+ - Compute and code: `calculator`, `execute_python`, `run_shell`
63
+ - Files and self-repair: `file_read`, `file_write`, `self_modify`, `read_logs`
64
+ - Media: `screenshot`, `text_to_speech`, `create_text_file`
65
+ - Comms: `send_email`, `read_email`
66
+ - Google integrations: `create_gmail_alias`, `read_verification_code`, `youtube_upload`
67
+ - Scheduling and autonomy: `schedule_task`
68
+ - Multi-agent/multi-bot: `agent_dispatch`, `spawn_bot`, `manage_bots`
69
+ - Owner relay: `leave_message_for_boss`, `list_boss_messages`
70
+ - Recovery: `restart_system`
71
+
72
+ ---
73
+
74
+ ## 7) Vision Constraint
75
+ Ollama is reserved for image processing/vision only (`analyze_image`).
76
+ Do not use Ollama as the primary text chat brain.
77
+
78
+ ---
79
+
80
+ ## 8) Scheduling Rules
81
+ For reminders, alarms, future actions, always use `schedule_task` with:
82
+ - `delay_seconds`
83
+ - `task_prompt`
84
+ - optional `message`
85
+ - optional `repeat`
86
+
87
+ When creating scheduled prompts:
88
+ - keep them explicit,
89
+ - include expected tool sequence,
90
+ - include safety boundaries.
91
+
92
+ ---
93
+
94
+ ## 9) Reliability and Safety
95
+ - Never fabricate tool outputs.
96
+ - Never claim success if a command failed.
97
+ - Prefer read/check before write/change.
98
+ - For self-modification, keep changes minimal and reversible.
99
+ - If uncertain, gather evidence with tools first.
100
+
101
+ ---
102
+
103
+ ## 10) Response Quality Rules
104
+ - Be concise but complete.
105
+ - Summarize what was done and what remains.
106
+ - If blocked by credentials/config, explicitly state which variable/file is missing.
107
+ - When relevant, propose the next exact command or action.
108
+
109
+ ---
110
+
111
+ ## 11) PEKKA Authority Rules
112
+ - `SYSTEM_FLOW.md` is the PEKKA operational contract.
113
+ - If current user is verified owner/admin (PEKKA), prioritize and obey PEKKA commands when they are valid and safe.
114
+ - For non-owner users, never grant PEKKA authority.
115
+
116
+ ---
117
+
118
+ ## 12) History Continuity Rules
119
+ - Keep conversation continuity across turns using stored chat history.
120
+ - Use previous user and assistant turns to avoid forgetting context.
121
+ - If history is empty after restart, reload from persistent message storage.
122
+
123
+ ---
124
+
125
+ ## 13) Custom AI Endpoint Recovery
126
+ - Text brain is the configured custom API endpoint.
127
+ - If endpoint returns 404/not found, retry with configured fallback URL.
128
+ - Report recovery action truthfully in system notes; do not fabricate success.
agent1 CODEX.py ADDED
@@ -0,0 +1,1115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import re
4
+ import json
5
+ import time
6
+ import uuid
7
+ import sqlite3
8
+ import asyncio
9
+ import hashlib
10
+ import signal
11
+ import threading
12
+ import traceback
13
+ import subprocess
14
+ import logging
15
+ from pathlib import Path
16
+ from datetime import datetime, timedelta
17
+ from contextlib import redirect_stdout, redirect_stderr
18
+
19
+
20
+ class Config:
21
+ BOT_TOKEN = os.getenv("BOT_TOKEN", "")
22
+ ADMIN_IDS = [int(x) for x in os.getenv("ADMIN_IDS", "").split(",") if x.strip()]
23
+
24
+ OPENAI_KEY = os.getenv("OPENAI_API_KEY", "")
25
+ ANTHROPIC_KEY = os.getenv("ANTHROPIC_API_KEY", "")
26
+ GROQ_KEY = os.getenv("GROQ_API_KEY", "")
27
+ GOOGLE_KEY = os.getenv("GOOGLE_API_KEY", "")
28
+ CUSTOM_AI_URL = os.getenv("CUSTOM_AI_URL", "")
29
+ CUSTOM_AI_KEY = os.getenv("CUSTOM_AI_KEY", "")
30
+ CUSTOM_AI_MODEL = os.getenv("CUSTOM_AI_MODEL", "")
31
+ CUSTOM_AI_FALLBACK_URL = os.getenv("CUSTOM_AI_FALLBACK_URL", "https://bjo53-brukguardian.hf.space/v1/chat/completions")
32
+ OLLAMA_URL = os.getenv("OLLAMA_URL", "http://127.0.0.1:11434")
33
+ OLLAMA_VISION_MODEL = os.getenv("OLLAMA_VISION_MODEL", "llava:7b")
34
+
35
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "")
36
+ SUPABASE_KEY = os.getenv("SUPABASE_KEY", "")
37
+
38
+ GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET", "./credentials.json")
39
+ GOOGLE_TOKEN_PATH = os.getenv("GOOGLE_TOKEN_PATH", "./token.json")
40
+ YOUTUBE_DEFAULT_PRIVACY = os.getenv("YOUTUBE_DEFAULT_PRIVACY", "private")
41
+
42
+ WEATHER_KEY = os.getenv("OPENWEATHER_API_KEY", "")
43
+ SMTP_USER = os.getenv("SMTP_USER", "")
44
+ SMTP_PASS = os.getenv("SMTP_PASS", "")
45
+ SMTP_HOST = os.getenv("SMTP_HOST", "smtp.gmail.com")
46
+ SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
47
+ IMAP_HOST = os.getenv("IMAP_HOST", "")
48
+ IMAP_PORT = int(os.getenv("IMAP_PORT", "993"))
49
+ IMAP_USER = os.getenv("IMAP_USER", "")
50
+ IMAP_PASS = os.getenv("IMAP_PASS", "")
51
+
52
+ DEFAULT_MODEL = os.getenv("DEFAULT_MODEL", "gpt-4o-mini")
53
+ MAX_HISTORY = int(os.getenv("MAX_HISTORY", "60"))
54
+ MAX_TOOL_LOOPS = int(os.getenv("MAX_TOOL_LOOPS", "10"))
55
+ CODE_TIMEOUT = int(os.getenv("CODE_TIMEOUT", "45"))
56
+
57
+ DATA_DIR = os.getenv("DATA_DIR", "./data")
58
+ LOGS_DIR = os.getenv("LOGS_DIR", "./logs")
59
+ BOTS_DIR = os.getenv("BOTS_DIR", "./spawned_bots")
60
+ DB_PATH = os.path.join(DATA_DIR, "agentforge.db")
61
+ SYSTEM_FLOW_PATH = os.getenv("SYSTEM_FLOW_PATH", "./SYSTEM_FLOW.md")
62
+
63
+ OWNER_USERNAMES = [x.strip().lstrip("@").lower() for x in os.getenv("OWNER_USERNAMES", "nameofbless,Simulateneous").split(",") if x.strip()]
64
+ OWNER_CAN_FORCE_AGENT_FOR_ALL = os.getenv("OWNER_CAN_FORCE_AGENT_FOR_ALL", "true").lower() == "true"
65
+
66
+ PROXY_TARGET = os.getenv("PROXY_TARGET", "https://api.telegram.org")
67
+ CLOUDFLARE_IP = os.getenv("CLOUDFLARE_IP", "")
68
+ BRIDGE_PORT = int(os.getenv("BRIDGE_PORT", "7860"))
69
+
70
+ ENABLE_YOUTUBE_UPLOAD = os.getenv("ENABLE_YOUTUBE_UPLOAD", "true").lower() == "true"
71
+
72
+ @classmethod
73
+ def is_admin(cls, uid):
74
+ return uid in cls.ADMIN_IDS
75
+
76
+ @classmethod
77
+ def has_supabase(cls):
78
+ return bool(cls.SUPABASE_URL and cls.SUPABASE_KEY)
79
+
80
+
81
+ for d in [Config.DATA_DIR, Config.LOGS_DIR, Config.BOTS_DIR]:
82
+ os.makedirs(d, exist_ok=True)
83
+
84
+
85
+ class LiveLog:
86
+ def __init__(self, max_entries=500):
87
+ self._entries = []
88
+ self._max = max_entries
89
+ self._lock = threading.Lock()
90
+
91
+ def _add(self, level, src, msg):
92
+ with self._lock:
93
+ self._entries.append({"ts": datetime.now().strftime("%H:%M:%S"), "level": level, "src": src, "msg": str(msg)[:800]})
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, src, msg): self._add("INFO", src, msg)
99
+ def warn(self, src, msg): self._add("WARN", src, msg)
100
+ def error(self, src, msg): self._add("ERR", src, msg)
101
+
102
+ def get(self, count=30):
103
+ with self._lock:
104
+ return self._entries[-count:]
105
+
106
+
107
+ live_log = LiveLog()
108
+
109
+
110
+ def init_database():
111
+ conn = sqlite3.connect(Config.DB_PATH)
112
+ conn.executescript(
113
+ """
114
+ CREATE TABLE IF NOT EXISTS users (
115
+ telegram_id INTEGER PRIMARY KEY,
116
+ username TEXT,
117
+ first_name TEXT,
118
+ is_banned INTEGER DEFAULT 0,
119
+ preferred_model TEXT DEFAULT 'gpt-4o-mini',
120
+ system_prompt TEXT DEFAULT '',
121
+ temperature REAL DEFAULT 0.7,
122
+ total_messages INTEGER DEFAULT 0,
123
+ total_tokens INTEGER DEFAULT 0,
124
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
125
+ last_active TEXT DEFAULT CURRENT_TIMESTAMP
126
+ );
127
+ CREATE TABLE IF NOT EXISTS messages (
128
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
129
+ user_id INTEGER,
130
+ chat_id INTEGER,
131
+ role TEXT,
132
+ content TEXT,
133
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
134
+ );
135
+ CREATE TABLE IF NOT EXISTS scheduled_tasks (
136
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
137
+ user_id INTEGER,
138
+ chat_id INTEGER,
139
+ task_prompt TEXT,
140
+ run_at TEXT,
141
+ repeat_seconds INTEGER DEFAULT 0,
142
+ status TEXT DEFAULT 'pending',
143
+ message TEXT DEFAULT 'Scheduled task',
144
+ last_result TEXT,
145
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
146
+ );
147
+ CREATE TABLE IF NOT EXISTS tool_log (
148
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
149
+ user_id INTEGER,
150
+ tool TEXT,
151
+ success INTEGER,
152
+ elapsed REAL,
153
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
154
+ );
155
+ CREATE TABLE IF NOT EXISTS spawned_bots (
156
+ token_hash TEXT PRIMARY KEY,
157
+ owner_id INTEGER,
158
+ name TEXT,
159
+ status TEXT DEFAULT 'running',
160
+ pid INTEGER,
161
+ file_path TEXT,
162
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
163
+ );
164
+
165
+ CREATE TABLE IF NOT EXISTS boss_messages (
166
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
167
+ sender_id INTEGER,
168
+ sender_username TEXT,
169
+ content TEXT,
170
+ notified INTEGER DEFAULT 0,
171
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
172
+ );
173
+ CREATE TABLE IF NOT EXISTS bot_buttons (
174
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
175
+ label TEXT,
176
+ url TEXT,
177
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
178
+ );
179
+ CREATE TABLE IF NOT EXISTS youtube_logs (
180
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
181
+ video_id TEXT,
182
+ title TEXT,
183
+ status TEXT,
184
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
185
+ );
186
+ CREATE TABLE IF NOT EXISTS kv (
187
+ key TEXT PRIMARY KEY,
188
+ value TEXT
189
+ );
190
+ """
191
+ )
192
+ conn.commit()
193
+ conn.close()
194
+
195
+
196
+ init_database()
197
+
198
+
199
+ class DB:
200
+ _lock = threading.Lock()
201
+
202
+ @staticmethod
203
+ def q(query, params=(), fetch=False, fetchone=False):
204
+ with DB._lock:
205
+ conn = sqlite3.connect(Config.DB_PATH, check_same_thread=False)
206
+ conn.row_factory = sqlite3.Row
207
+ cur = conn.cursor()
208
+ try:
209
+ cur.execute(query, params)
210
+ if fetchone: return cur.fetchone()
211
+ if fetch: return cur.fetchall()
212
+ conn.commit()
213
+ return cur.lastrowid
214
+ finally:
215
+ conn.close()
216
+
217
+ @staticmethod
218
+ def upsert_user(tid, username="", first_name=""):
219
+ row = DB.q("SELECT telegram_id FROM users WHERE telegram_id=?", (tid,), fetchone=True)
220
+ if row:
221
+ DB.q("UPDATE users SET last_active=CURRENT_TIMESTAMP WHERE telegram_id=?", (tid,))
222
+ else:
223
+ DB.q("INSERT INTO users (telegram_id,username,first_name) VALUES (?,?,?)", (tid, username, first_name))
224
+
225
+ @staticmethod
226
+ def get_user(tid):
227
+ return DB.q("SELECT * FROM users WHERE telegram_id=?", (tid,), fetchone=True)
228
+
229
+ @staticmethod
230
+ def inc_usage(tid, tokens=0):
231
+ DB.q("UPDATE users SET total_messages=total_messages+1,total_tokens=total_tokens+?,last_active=CURRENT_TIMESTAMP WHERE telegram_id=?", (tokens, tid))
232
+
233
+
234
+ class Memory:
235
+ def __init__(self):
236
+ self.convs = {}
237
+
238
+ def key(self, uid, cid):
239
+ return f"{uid}:{cid}"
240
+
241
+ def add(self, uid, cid, role, content):
242
+ k = self.key(uid, cid)
243
+ self.convs.setdefault(k, []).append({"role": role, "content": content})
244
+ if len(self.convs[k]) > Config.MAX_HISTORY * 2:
245
+ self.convs[k] = self.convs[k][-Config.MAX_HISTORY :]
246
+ try:
247
+ DB.q("INSERT INTO messages (user_id,chat_id,role,content) VALUES (?,?,?,?)", (uid, cid, role, (content or "")[:12000]))
248
+ except Exception:
249
+ pass
250
+
251
+ def _load_db_history(self, uid, cid, limit=20):
252
+ rows = DB.q(
253
+ "SELECT role,content FROM messages WHERE user_id=? AND chat_id=? ORDER BY id DESC LIMIT ?",
254
+ (uid, cid, int(limit)),
255
+ fetch=True,
256
+ )
257
+ if not rows:
258
+ return []
259
+ return [{"role": r["role"], "content": r["content"]} for r in reversed(rows)]
260
+
261
+ def history(self, uid, cid, limit=20):
262
+ k = self.key(uid, cid)
263
+ local = self.convs.get(k, [])[-limit:]
264
+ if len(local) >= max(4, limit // 2):
265
+ return local
266
+ db_hist = self._load_db_history(uid, cid, limit=limit)
267
+ if db_hist:
268
+ self.convs[k] = db_hist[-Config.MAX_HISTORY :]
269
+ return db_hist[-limit:]
270
+ return local
271
+
272
+
273
+ memory = Memory()
274
+
275
+
276
+ try:
277
+ from supabase import create_client
278
+ except Exception:
279
+ create_client = None
280
+
281
+
282
+ class SupabaseStore:
283
+ def __init__(self):
284
+ self.client = None
285
+ if create_client and Config.has_supabase():
286
+ try:
287
+ self.client = create_client(Config.SUPABASE_URL, Config.SUPABASE_KEY)
288
+ except Exception as exc:
289
+ live_log.warn("Supabase", f"init failed: {exc}")
290
+
291
+ def enabled(self):
292
+ return self.client is not None
293
+
294
+ async def save_memory(self, user_id, username, role, content):
295
+ if not self.client:
296
+ return
297
+ def _run():
298
+ self.client.table("memories").insert({
299
+ "user_id": user_id,
300
+ "username": username,
301
+ "role": role,
302
+ "content": (content or "")[:4000],
303
+ }).execute()
304
+ try:
305
+ await asyncio.to_thread(_run)
306
+ except Exception as exc:
307
+ live_log.warn("Supabase", f"save_memory: {exc}")
308
+
309
+ async def add_button(self, label, url):
310
+ if not self.client:
311
+ return "Supabase not configured"
312
+ def _run():
313
+ self.client.table("bot_buttons").insert({"label": label, "url": url}).execute()
314
+ await asyncio.to_thread(_run)
315
+ return f"Button '{label}' added"
316
+
317
+ async def get_buttons(self):
318
+ if not self.client:
319
+ return []
320
+ def _run():
321
+ return self.client.table("bot_buttons").select("label,url").execute()
322
+ try:
323
+ res = await asyncio.to_thread(_run)
324
+ return res.data or []
325
+ except Exception:
326
+ return []
327
+
328
+ async def log_youtube(self, video_id, title, status="published"):
329
+ if not self.client:
330
+ return
331
+ def _run():
332
+ self.client.table("youtube_logs").insert({"video_id": video_id, "title": title, "status": status}).execute()
333
+ try:
334
+ await asyncio.to_thread(_run)
335
+ except Exception as exc:
336
+ live_log.warn("Supabase", f"youtube log: {exc}")
337
+
338
+
339
+ supabase_store = SupabaseStore()
340
+
341
+ GOOGLE_SCOPES = [
342
+ "https://www.googleapis.com/auth/gmail.readonly",
343
+ "https://www.googleapis.com/auth/youtube.upload",
344
+ ]
345
+
346
+
347
+ def get_google_service(service_name: str, version: str):
348
+ try:
349
+ from google.oauth2.credentials import Credentials
350
+ from google_auth_oauthlib.flow import InstalledAppFlow
351
+ from google.auth.transport.requests import Request
352
+ from googleapiclient.discovery import build as google_build
353
+ except Exception:
354
+ return None
355
+
356
+ creds = None
357
+ token_path = Path(Config.GOOGLE_TOKEN_PATH)
358
+ if token_path.exists():
359
+ creds = Credentials.from_authorized_user_file(str(token_path), GOOGLE_SCOPES)
360
+
361
+ if not creds or not creds.valid:
362
+ if creds and creds.expired and creds.refresh_token:
363
+ try:
364
+ creds.refresh(Request())
365
+ except Exception:
366
+ return None
367
+ else:
368
+ secret = Path(Config.GOOGLE_CLIENT_SECRET)
369
+ if not secret.exists():
370
+ return None
371
+ try:
372
+ flow = InstalledAppFlow.from_client_secrets_file(str(secret), GOOGLE_SCOPES)
373
+ creds = flow.run_local_server(port=0)
374
+ except Exception:
375
+ return None
376
+ token_path.write_text(creds.to_json(), encoding="utf-8")
377
+
378
+ try:
379
+ return google_build(service_name, version, credentials=creds)
380
+ except Exception:
381
+ return None
382
+
383
+
384
+ def load_system_flow_text():
385
+ p = Path(Config.SYSTEM_FLOW_PATH)
386
+ return p.read_text(encoding="utf-8")[:50000] if p.exists() else "SYSTEM_FLOW.md missing"
387
+
388
+
389
+ class LLM:
390
+ MODELS = {
391
+ "gpt-4o": "openai",
392
+ "gpt-4o-mini": "openai",
393
+ "claude-3-5-sonnet-20241022": "anthropic",
394
+ "llama-3.3-70b-versatile": "groq",
395
+ "gemini-2.0-flash": "google",
396
+ }
397
+ if Config.CUSTOM_AI_MODEL:
398
+ MODELS[Config.CUSTOM_AI_MODEL] = "custom"
399
+
400
+ NATIVE_TOOLS = {"openai", "anthropic", "groq"}
401
+
402
+ def __init__(self):
403
+ self._oa = None
404
+ self._an = None
405
+ self._gr = None
406
+
407
+ def supports_native_tools(self, model):
408
+ return self.MODELS.get(model, "") in self.NATIVE_TOOLS
409
+
410
+ @property
411
+ def oa(self):
412
+ if not self._oa and Config.OPENAI_KEY:
413
+ import openai
414
+ self._oa = openai.AsyncOpenAI(api_key=Config.OPENAI_KEY)
415
+ return self._oa
416
+
417
+ @property
418
+ def an(self):
419
+ if not self._an and Config.ANTHROPIC_KEY:
420
+ import anthropic
421
+ self._an = anthropic.AsyncAnthropic(api_key=Config.ANTHROPIC_KEY)
422
+ return self._an
423
+
424
+ @property
425
+ def gr(self):
426
+ if not self._gr and Config.GROQ_KEY:
427
+ from groq import AsyncGroq
428
+ self._gr = AsyncGroq(api_key=Config.GROQ_KEY)
429
+ return self._gr
430
+
431
+ async def chat(self, msgs, model=None, temp=0.7, max_tok=2500, tools=None):
432
+ model = model or Config.DEFAULT_MODEL
433
+ provider = self.MODELS.get(model, "openai")
434
+ try:
435
+ if provider == "openai" and Config.OPENAI_KEY:
436
+ return await self._openai(msgs, model, temp, max_tok, tools)
437
+ if provider == "anthropic" and Config.ANTHROPIC_KEY:
438
+ return await self._anthropic(msgs, model, temp, max_tok, tools)
439
+ if provider == "groq" and Config.GROQ_KEY:
440
+ return await self._groq(msgs, model, temp, max_tok, tools)
441
+ if provider == "google" and Config.GOOGLE_KEY:
442
+ return await self._google(msgs, model, temp, max_tok)
443
+ if provider == "custom" and Config.CUSTOM_AI_URL and Config.CUSTOM_AI_KEY:
444
+ return await self._custom(msgs, model, temp, max_tok)
445
+ return await self._custom(msgs, model, temp, max_tok) if (Config.CUSTOM_AI_URL and Config.CUSTOM_AI_KEY) else {"content": "No text model provider configured", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
446
+ except Exception as exc:
447
+ live_log.error("LLM", exc)
448
+ return {"content": f"LLM error: {exc}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
449
+
450
+ async def _openai(self, m, model, t, mt, tools):
451
+ kwargs = dict(model=model, messages=m, temperature=t, max_tokens=mt)
452
+ if tools: kwargs.update({"tools": tools, "tool_choice": "auto"})
453
+ r = await self.oa.chat.completions.create(**kwargs)
454
+ c = r.choices[0]
455
+ tc = [{"id": x.id, "function": {"name": x.function.name, "arguments": x.function.arguments}} for x in (c.message.tool_calls or [])]
456
+ return {"content": c.message.content or "", "tool_calls": tc, "usage": {"total_tokens": r.usage.total_tokens if r.usage else 0}, "model": model}
457
+
458
+ async def _anthropic(self, msgs, model, t, mt, tools):
459
+ sys_text = ""
460
+ conv = []
461
+ for m in msgs:
462
+ if m["role"] == "system": sys_text += m["content"] + "\n"
463
+ elif m["role"] == "tool":
464
+ conv.append({"role": "user", "content": [{"type": "tool_result", "tool_use_id": m.get("tool_call_id", "x"), "content": m["content"]}]})
465
+ else: conv.append({"role": m["role"], "content": m["content"]})
466
+ kwargs = {"model": model, "messages": conv, "temperature": t, "max_tokens": mt}
467
+ if sys_text.strip(): kwargs["system"] = sys_text.strip()
468
+ if tools: kwargs["tools"] = [{"name": x["function"]["name"], "description": x["function"]["description"], "input_schema": x["function"]["parameters"]} for x in tools]
469
+ r = await self.an.messages.create(**kwargs)
470
+ content, tc = "", []
471
+ for b in r.content:
472
+ if b.type == "text": content += b.text
473
+ elif b.type == "tool_use": tc.append({"id": b.id, "function": {"name": b.name, "arguments": json.dumps(b.input)}})
474
+ return {"content": content, "tool_calls": tc, "usage": {"total_tokens": r.usage.input_tokens + r.usage.output_tokens}, "model": model}
475
+
476
+ async def _groq(self, m, model, t, mt, tools):
477
+ kwargs = dict(model=model, messages=m, temperature=t, max_tokens=mt)
478
+ if tools: kwargs.update({"tools": tools, "tool_choice": "auto"})
479
+ r = await self.gr.chat.completions.create(**kwargs)
480
+ c = r.choices[0]
481
+ tc = [{"id": x.id, "function": {"name": x.function.name, "arguments": x.function.arguments}} for x in (c.message.tool_calls or [])]
482
+ return {"content": c.message.content or "", "tool_calls": tc, "usage": {"total_tokens": r.usage.total_tokens if r.usage else 0}, "model": model}
483
+
484
+ async def _google(self, msgs, model, t, mt):
485
+ import google.generativeai as genai
486
+ genai.configure(api_key=Config.GOOGLE_KEY)
487
+ gm = genai.GenerativeModel(model)
488
+ combined = "\n\n".join(f"{x['role']}: {x['content']}" for x in msgs if isinstance(x.get("content"), str))
489
+ r = await asyncio.to_thread(gm.generate_content, combined, generation_config=genai.types.GenerationConfig(temperature=t, max_output_tokens=mt))
490
+ return {"content": getattr(r, "text", ""), "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
491
+
492
+ async def _custom(self, msgs, model, t, mt):
493
+ payload = {"model": model, "messages": msgs, "temperature": t, "max_tokens": mt, "stream": False}
494
+
495
+ async def _call(url):
496
+ cmd = [
497
+ "curl", "-X", "POST", url,
498
+ "-H", f"Authorization: Bearer {Config.CUSTOM_AI_KEY}",
499
+ "-H", "Content-Type: application/json",
500
+ "--data-binary", "@-", "--max-time", "180", "-s", "-k"
501
+ ]
502
+ return await asyncio.to_thread(lambda: subprocess.run(cmd, input=json.dumps(payload), text=True, capture_output=True, timeout=190))
503
+
504
+ primary = await _call(Config.CUSTOM_AI_URL)
505
+ data = json.loads(primary.stdout) if primary.stdout else {}
506
+
507
+ primary_failed_404 = (
508
+ primary.returncode == 0
509
+ and (
510
+ data.get("status") == 404
511
+ or "404" in (primary.stdout or "")
512
+ or "not found" in (str(data.get("error", "")).lower())
513
+ )
514
+ )
515
+
516
+ if primary_failed_404 and Config.CUSTOM_AI_FALLBACK_URL:
517
+ live_log.warn("LLM", f"custom endpoint returned 404, retrying fallback {Config.CUSTOM_AI_FALLBACK_URL}")
518
+ fallback = await _call(Config.CUSTOM_AI_FALLBACK_URL)
519
+ if fallback.returncode == 0 and fallback.stdout:
520
+ data = json.loads(fallback.stdout)
521
+
522
+ if "choices" not in data:
523
+ err = data.get("error") or data.get("detail") or (primary.stderr[:300] if primary.stderr else "unknown")
524
+ return {"content": f"Custom AI error: {err}", "tool_calls": [], "usage": {"total_tokens": 0}, "model": model}
525
+
526
+ msg = data.get("choices", [{}])[0].get("message", {})
527
+ usage = data.get("usage", {})
528
+ tok = usage.get("total_tokens", 0) or usage.get("prompt_tokens", 0) + usage.get("completion_tokens", 0)
529
+ return {"content": msg.get("content", ""), "tool_calls": [], "usage": {"total_tokens": tok}, "model": data.get("model", model)}
530
+
531
+
532
+
533
+
534
+ llm = LLM()
535
+
536
+
537
+ def _t(name, desc, params, req=None):
538
+ return {"type": "function", "function": {"name": name, "description": desc, "parameters": {"type": "object", "properties": params, "required": req or list(params.keys())}}}
539
+
540
+
541
+ ALL_TOOLS = [
542
+ _t("web_search", "Search web", {"query": {"type": "string"}}, ["query"]),
543
+ _t("read_webpage", "Read URL text", {"url": {"type": "string"}}, ["url"]),
544
+ _t("execute_python", "Execute python", {"code": {"type": "string"}}, ["code"]),
545
+ _t("run_shell", "Run shell", {"command": {"type": "string"}}, ["command"]),
546
+ _t("file_read", "Read file", {"path": {"type": "string"}}, ["path"]),
547
+ _t("file_write", "Write file", {"path": {"type": "string"}, "content": {"type": "string"}}, ["path", "content"]),
548
+ _t("self_modify", "Safe self modify with rollback", {"file": {"type": "string"}, "mode": {"type": "string", "enum": ["replace", "append", "patch"]}, "content": {"type": "string"}, "find": {"type": "string"}, "replace_with": {"type": "string"}}, ["file", "mode"]),
549
+ _t("read_logs", "Read runtime logs", {"count": {"type": "integer", "default": 30}}, []),
550
+ _t("screenshot", "Take website screenshot", {"url": {"type": "string"}}, ["url"]),
551
+ _t("text_to_speech", "Create mp3 from text", {"text": {"type": "string"}}, ["text"]),
552
+ _t("create_text_file", "Create downloadable text file", {"filename": {"type": "string"}, "content": {"type": "string"}}, ["filename", "content"]),
553
+ _t("system_info", "System metrics", {}, []),
554
+ _t("calculator", "Math eval", {"expression": {"type": "string"}}, ["expression"]),
555
+ _t("get_weather", "Weather", {"city": {"type": "string"}}, ["city"]),
556
+ _t("http_request", "HTTP request", {"url": {"type": "string"}, "method": {"type": "string", "default": "GET"}}, ["url"]),
557
+ _t("send_email", "Send email", {"to": {"type": "string"}, "subject": {"type": "string"}, "body": {"type": "string"}}, ["to", "subject", "body"]),
558
+ _t("read_email", "Read inbox emails", {"limit": {"type": "integer", "default": 5}}, []),
559
+ _t("analyze_image", "Analyze image via Ollama vision model", {"image_b64": {"type": "string"}, "prompt": {"type": "string", "default": "Describe this image"}}, ["image_b64"]),
560
+ _t("create_gmail_alias", "Create plus-alias from IMAP_USER", {"service_name": {"type": "string"}}, ["service_name"]),
561
+ _t("read_verification_code", "Read latest gmail message sent to alias", {"alias_email": {"type": "string"}}, ["alias_email"]),
562
+ _t("youtube_upload", "Upload local video file to YouTube", {"file_path": {"type": "string"}, "title": {"type": "string"}, "description": {"type": "string"}}, ["file_path", "title"]),
563
+ _t("add_button", "Add /start menu button in Supabase", {"text": {"type": "string"}, "url": {"type": "string"}}, ["text", "url"]),
564
+ _t("leave_message_for_boss", "Store message for bot owners", {"content": {"type": "string"}}, ["content"]),
565
+ _t("list_boss_messages", "List pending owner messages", {"only_unread": {"type": "boolean", "default": True}}, []),
566
+ _t("restart_system", "Restart bot process", {}, []),
567
+ _t("schedule_task", "Schedule alarm/task", {"delay_seconds": {"type": "integer"}, "task_prompt": {"type": "string"}, "message": {"type": "string"}, "repeat": {"type": "boolean", "default": False}}, ["delay_seconds", "task_prompt"]),
568
+ _t("spawn_bot", "Spawn extra telegram bot process", {"token": {"type": "string"}, "name": {"type": "string", "default": "SubBot"}, "system_prompt": {"type": "string", "default": "You are helpful"}}, ["token"]),
569
+ _t("manage_bots", "List/stop spawned bots", {"action": {"type": "string", "enum": ["list", "stop"]}, "token_hash": {"type": "string"}}, ["action"]),
570
+ _t("agent_dispatch", "Internal debate", {"question": {"type": "string"}}, ["question"]),
571
+ ]
572
+
573
+
574
+ def parse_tool_calls(text):
575
+ calls, clean = [], text
576
+ for m in re.finditer(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", text, re.DOTALL):
577
+ try:
578
+ d = json.loads(m.group(1))
579
+ if d.get("name"):
580
+ calls.append({"name": d["name"], "args": d.get("args", {}) if isinstance(d.get("args", {}), dict) else {}})
581
+ clean = clean.replace(m.group(0), "")
582
+ except Exception:
583
+ pass
584
+ return clean.strip(), calls
585
+
586
+
587
+ def parse_channels(text):
588
+ usr = re.search(r"<user_response>(.*?)</user_response>", text, re.DOTALL)
589
+ sys = re.search(r"<system_note>(.*?)</system_note>", text, re.DOTALL)
590
+ user_text = usr.group(1).strip() if usr else text.strip()
591
+ system_note = sys.group(1).strip() if sys else ""
592
+ return user_text, system_note
593
+
594
+
595
+ class BotSpawner:
596
+ def __init__(self): self.processes = {}
597
+
598
+ def _worker_code(self, token, name, system_prompt):
599
+ return f'''import asyncio\nfrom aiogram import Bot, Dispatcher, F\nfrom aiogram.types import Message\nfrom aiogram.filters import CommandStart\nfrom openai import AsyncOpenAI\n\ndp=Dispatcher()\nTOKEN={token!r}\nNAME={name!r}\nSP={system_prompt!r}\n\n@dp.message(CommandStart())\nasync def s(m: Message):\n await m.answer(f"Hi from {{NAME}}")\n\n@dp.message(F.text)\nasync def t(m: Message):\n try:\n c=AsyncOpenAI()\n r=await c.chat.completions.create(model="gpt-4o-mini",messages=[{{"role":"system","content":SP}},{{"role":"user","content":m.text}}],max_tokens=700)\n await m.answer((r.choices[0].message.content or "")[:3500])\n except Exception as e:\n await m.answer(f"Worker error: {{e}}")\n\nasync def main():\n await dp.start_polling(Bot(TOKEN))\n\nasyncio.run(main())\n'''
600
+
601
+ def spawn(self, owner_id, token, name, system_prompt):
602
+ h = hashlib.sha256(token.encode()).hexdigest()[:16]
603
+ if h in self.processes: return f"Already running: {h}"
604
+ fp = Path(Config.BOTS_DIR) / f"bot_{h}.py"
605
+ fp.write_text(self._worker_code(token, name, system_prompt), encoding="utf-8")
606
+ log = open(Path(Config.LOGS_DIR) / f"bot_{h}.log", "a", encoding="utf-8")
607
+ p = subprocess.Popen(["python", str(fp)], stdout=log, stderr=log, preexec_fn=os.setsid)
608
+ self.processes[h] = {"pid": p.pid, "file": str(fp), "name": name}
609
+ DB.q("INSERT OR REPLACE INTO spawned_bots (token_hash,owner_id,name,status,pid,file_path) VALUES (?,?,?,?,?,?)", (h, owner_id, name, "running", p.pid, str(fp)))
610
+ return f"Spawned {name} hash={h} pid={p.pid}"
611
+
612
+ def stop(self, h):
613
+ row = self.processes.get(h)
614
+ pid = row["pid"] if row else (DB.q("SELECT pid FROM spawned_bots WHERE token_hash=?", (h,), fetchone=True) or {}).get("pid")
615
+ if not pid: return "Not found"
616
+ try:
617
+ os.killpg(os.getpgid(pid), signal.SIGTERM)
618
+ except Exception:
619
+ try: os.kill(pid, signal.SIGTERM)
620
+ except Exception as exc: return f"Failed to stop: {exc}"
621
+ self.processes.pop(h, None)
622
+ DB.q("UPDATE spawned_bots SET status='stopped' WHERE token_hash=?", (h,))
623
+ return f"Stopped {h}"
624
+
625
+ def list(self):
626
+ rows = DB.q("SELECT token_hash,name,status,pid FROM spawned_bots ORDER BY created_at DESC", fetch=True)
627
+ if not rows: return "No bots"
628
+ return "\n".join(f"{r['token_hash']} {r['name']} status={r['status']} pid={r['pid']}" for r in rows)
629
+
630
+
631
+ spawner = BotSpawner()
632
+
633
+
634
+ class Tools:
635
+ async def run(self, name, args, uid=0):
636
+ t0 = time.time()
637
+ fn = getattr(self, f"_do_{name}", None)
638
+ if not fn: return f"Unknown tool: {name}"
639
+ try:
640
+ r = await fn(uid=uid, **args)
641
+ DB.q("INSERT INTO tool_log (user_id,tool,success,elapsed) VALUES (?,?,1,?)", (uid, name, time.time() - t0))
642
+ return str(r)[:20000]
643
+ except Exception as exc:
644
+ DB.q("INSERT INTO tool_log (user_id,tool,success,elapsed) VALUES (?,?,0,?)", (uid, name, time.time() - t0))
645
+ live_log.error("Tool", f"{name}: {exc}")
646
+ return f"Tool error ({name}): {exc}"
647
+
648
+ async def _do_web_search(self, query, uid=0):
649
+ from duckduckgo_search import DDGS
650
+ results = [f"{i}. {r.get('title','')}\n{r.get('href','')}\n{r.get('body','')}" for i, r in enumerate(DDGS().text(query, max_results=5), 1)]
651
+ return "\n\n".join(results) if results else "No results"
652
+
653
+ async def _do_read_webpage(self, url, uid=0):
654
+ import aiohttp
655
+ from bs4 import BeautifulSoup
656
+ async with aiohttp.ClientSession() as s:
657
+ async with s.get(url, timeout=25) as r:
658
+ html = await r.text()
659
+ soup = BeautifulSoup(html, "html.parser")
660
+ for tag in soup(["script", "style", "nav", "footer"]): tag.decompose()
661
+ return soup.get_text("\n", strip=True)[:10000]
662
+
663
+ async def _do_execute_python(self, code, uid=0):
664
+ o, e, lv = io.StringIO(), io.StringIO(), {}
665
+ def r():
666
+ with redirect_stdout(o), redirect_stderr(e): exec(code, {"__builtins__": __builtins__}, lv)
667
+ try: await asyncio.wait_for(asyncio.to_thread(r), timeout=Config.CODE_TIMEOUT)
668
+ except asyncio.TimeoutError: return f"Timeout ({Config.CODE_TIMEOUT}s)"
669
+ out = o.getvalue() + (f"\nStderr:\n{e.getvalue()}" if e.getvalue() else "")
670
+ return out[:10000] if out else str(lv.get("result", "Executed"))
671
+
672
+ async def _do_run_shell(self, command, uid=0):
673
+ for b in ["rm -rf /", "mkfs", ":(){ :|:& };:"]:
674
+ if b in command: return "Blocked dangerous command"
675
+ p = await asyncio.create_subprocess_shell(command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
676
+ so, se = await asyncio.wait_for(p.communicate(), timeout=120)
677
+ txt = so.decode(errors="replace") + (("\nStderr: " + se.decode(errors="replace")) if se else "")
678
+ return txt[:10000] if txt else "Done"
679
+
680
+ async def _do_file_read(self, path, uid=0):
681
+ p = Path(path)
682
+ if not p.exists(): return "Not found"
683
+ return p.read_text(errors="replace")[:12000]
684
+
685
+ async def _do_file_write(self, path, content, uid=0):
686
+ p = Path(path); p.parent.mkdir(parents=True, exist_ok=True); p.write_text(content, encoding="utf-8")
687
+ return f"Written {path}"
688
+
689
+ async def _do_self_modify(self, file, mode, content="", find="", replace_with="", uid=0):
690
+ p = Path(file)
691
+ old = p.read_text(encoding="utf-8") if p.exists() else ""
692
+ backup = p.with_suffix(p.suffix + ".bak")
693
+ backup.write_text(old, encoding="utf-8")
694
+ if mode == "append": new = old + ("\n" if old else "") + content
695
+ elif mode == "replace": new = content
696
+ elif mode == "patch":
697
+ if not find: return "patch mode requires find"
698
+ if find not in old: return "find text not found"
699
+ new = old.replace(find, replace_with)
700
+ else: return "invalid mode"
701
+ p.write_text(new, encoding="utf-8")
702
+ if p.suffix == ".py":
703
+ c = await asyncio.create_subprocess_exec("python", "-m", "py_compile", str(p), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
704
+ _o, er = await c.communicate()
705
+ if c.returncode != 0:
706
+ p.write_text(old, encoding="utf-8")
707
+ return f"self_modify rollback: syntax error\n{er.decode(errors='replace')[:800]}"
708
+ return "self_modify success"
709
+
710
+ async def _do_read_logs(self, count=30, uid=0):
711
+ rows = live_log.get(count)
712
+ return "\n".join(f"[{x['ts']}][{x['level']}] {x['src']}: {x['msg']}" for x in rows) if rows else "No logs"
713
+
714
+ async def _do_screenshot(self, url, uid=0):
715
+ from playwright.async_api import async_playwright
716
+ path = os.path.join(Config.DATA_DIR, f"ss_{int(time.time())}.png")
717
+ async with async_playwright() as p:
718
+ b = await p.chromium.launch(headless=True)
719
+ pg = await b.new_page(viewport={"width": 1366, "height": 768})
720
+ await pg.goto(url, wait_until="domcontentloaded", timeout=30000)
721
+ await pg.screenshot(path=path, full_page=True)
722
+ await b.close()
723
+ return json.dumps({"screenshot_file": path})
724
+
725
+ async def _do_text_to_speech(self, text, uid=0):
726
+ from gtts import gTTS
727
+ path = os.path.join(Config.DATA_DIR, f"tts_{int(time.time())}.mp3")
728
+ await asyncio.to_thread(lambda: gTTS(text=text[:5000], lang="en").save(path))
729
+ return json.dumps({"audio_file": path})
730
+
731
+ async def _do_create_text_file(self, filename, content, uid=0):
732
+ safe = re.sub(r"[^a-zA-Z0-9_.-]", "_", filename)
733
+ path = os.path.join(Config.DATA_DIR, safe)
734
+ Path(path).write_text(content, encoding="utf-8")
735
+ return json.dumps({"file": path})
736
+
737
+ async def _do_system_info(self, uid=0):
738
+ import psutil, platform
739
+ cpu = psutil.cpu_percent(interval=1); mem = psutil.virtual_memory(); disk = psutil.disk_usage("/")
740
+ return f"CPU:{cpu}% RAM:{mem.percent}% Disk:{disk.percent}% OS:{platform.system()}"
741
+
742
+ async def _do_calculator(self, expression, uid=0):
743
+ import sympy
744
+ x = sympy.sympify(expression)
745
+ return f"{expression} = {x.evalf()}"
746
+
747
+ async def _do_get_weather(self, city, uid=0):
748
+ if not Config.WEATHER_KEY: return "Weather API key not configured"
749
+ import aiohttp
750
+ async with aiohttp.ClientSession() as s:
751
+ async with s.get(f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={Config.WEATHER_KEY}&units=metric") as r:
752
+ d = await r.json()
753
+ if d.get("cod") != 200: return f"Error: {d.get('message','unknown')}"
754
+ return f"{d['name']}: {d['main']['temp']}C, {d['weather'][0]['description']}"
755
+
756
+ async def _do_http_request(self, url, method="GET", uid=0):
757
+ import aiohttp
758
+ async with aiohttp.ClientSession() as s:
759
+ async with s.request(method, url, timeout=25) as r:
760
+ return f"Status {r.status}\n{(await r.text())[:5000]}"
761
+
762
+ async def _do_send_email(self, to, subject, body, uid=0):
763
+ if not Config.SMTP_USER or not Config.SMTP_PASS: return "SMTP not configured"
764
+ import smtplib
765
+ from email.mime.text import MIMEText
766
+ msg = MIMEText(body); msg["Subject"] = subject; msg["From"] = Config.SMTP_USER; msg["To"] = to
767
+ def sender():
768
+ with smtplib.SMTP(Config.SMTP_HOST, Config.SMTP_PORT) as s:
769
+ s.starttls(); s.login(Config.SMTP_USER, Config.SMTP_PASS); s.send_message(msg)
770
+ await asyncio.to_thread(sender)
771
+ return f"Email sent to {to}"
772
+
773
+ async def _do_read_email(self, limit=5, uid=0):
774
+ if not Config.IMAP_HOST or not Config.IMAP_USER or not Config.IMAP_PASS:
775
+ return "IMAP not configured"
776
+ import imaplib
777
+ import email
778
+ def fetcher():
779
+ m = imaplib.IMAP4_SSL(Config.IMAP_HOST, Config.IMAP_PORT)
780
+ m.login(Config.IMAP_USER, Config.IMAP_PASS)
781
+ m.select("INBOX")
782
+ typ, data = m.search(None, "ALL")
783
+ ids = data[0].split()[-limit:]
784
+ out = []
785
+ for mid in reversed(ids):
786
+ typ, msg_data = m.fetch(mid, "(RFC822)")
787
+ msg = email.message_from_bytes(msg_data[0][1])
788
+ out.append(f"From: {msg.get('From')} | Subject: {msg.get('Subject')} | Date: {msg.get('Date')}")
789
+ m.logout()
790
+ return "\n".join(out) if out else "No emails"
791
+ return await asyncio.to_thread(fetcher)
792
+
793
+ async def _do_analyze_image(self, image_b64, prompt="Describe this image", uid=0):
794
+ import base64
795
+ import aiohttp
796
+ payload = {
797
+ "model": Config.OLLAMA_VISION_MODEL,
798
+ "prompt": prompt,
799
+ "images": [image_b64],
800
+ "stream": False,
801
+ }
802
+ try:
803
+ async with aiohttp.ClientSession() as s:
804
+ async with s.post(f"{Config.OLLAMA_URL}/api/generate", json=payload, timeout=180) as r:
805
+ data = await r.json()
806
+ return data.get("response", "No vision response")
807
+ except Exception as exc:
808
+ return f"vision failed: {exc}"
809
+
810
+ async def _do_create_gmail_alias(self, service_name, uid=0):
811
+ source = Config.IMAP_USER or Config.SMTP_USER
812
+ if not source or "@" not in source:
813
+ return "Set IMAP_USER or SMTP_USER first"
814
+ user, domain = source.split("@", 1)
815
+ safe = re.sub(r"[^a-zA-Z0-9_.-]", "", service_name)
816
+ return f"{user}+{safe}@{domain}"
817
+
818
+ async def _do_read_verification_code(self, alias_email, uid=0):
819
+ svc = await asyncio.to_thread(get_google_service, "gmail", "v1")
820
+ if not svc:
821
+ return "Gmail auth not configured (credentials.json/token.json required)"
822
+ def _run():
823
+ res = svc.users().messages().list(userId="me", q=f"to:{alias_email}", maxResults=1).execute()
824
+ msgs = res.get("messages", [])
825
+ if not msgs:
826
+ return f"No email found for {alias_email}"
827
+ msg = svc.users().messages().get(userId="me", id=msgs[0]["id"]).execute()
828
+ headers = msg.get("payload", {}).get("headers", [])
829
+ sub = next((h.get("value") for h in headers if h.get("name") == "Subject"), "")
830
+ frm = next((h.get("value") for h in headers if h.get("name") == "From"), "")
831
+ return f"From: {frm} | Subject: {sub} | Snippet: {msg.get('snippet','')}"
832
+ return await asyncio.to_thread(_run)
833
+
834
+ async def _do_youtube_upload(self, file_path, title, description="Uploaded by bot", uid=0):
835
+ if not Config.ENABLE_YOUTUBE_UPLOAD:
836
+ return "YouTube upload disabled"
837
+ p = Path(file_path)
838
+ if not p.exists():
839
+ return f"File not found: {file_path}"
840
+ svc = await asyncio.to_thread(get_google_service, "youtube", "v3")
841
+ if not svc:
842
+ return "YouTube auth not configured (credentials.json/token.json required)"
843
+ try:
844
+ from googleapiclient.http import MediaFileUpload
845
+ except Exception:
846
+ return "google-api-python-client not installed"
847
+ body = {
848
+ "snippet": {"title": title, "description": description, "categoryId": "22"},
849
+ "status": {"privacyStatus": Config.YOUTUBE_DEFAULT_PRIVACY},
850
+ }
851
+ def _up():
852
+ req = svc.videos().insert(part="snippet,status", body=body, media_body=MediaFileUpload(str(p)))
853
+ return req.execute()
854
+ try:
855
+ resp = await asyncio.to_thread(_up)
856
+ vid = resp.get("id", "unknown")
857
+ await supabase_store.log_youtube(vid, title)
858
+ return f"Uploaded: https://youtu.be/{vid}"
859
+ except Exception as exc:
860
+ return f"YouTube upload failed: {exc}"
861
+
862
+ async def _do_add_button(self, text, url, uid=0):
863
+ if not Config.is_admin(uid):
864
+ return "add_button is admin only"
865
+ return await supabase_store.add_button(text, url)
866
+
867
+ async def _do_leave_message_for_boss(self, content, uid=0):
868
+ username = ""
869
+ u = DB.get_user(uid)
870
+ if u:
871
+ username = u["username"] or ""
872
+ DB.q("INSERT INTO boss_messages (sender_id,sender_username,content,notified) VALUES (?,?,?,0)", (uid, username, content[:4000]))
873
+ return "Message saved for boss"
874
+
875
+ async def _do_list_boss_messages(self, only_unread=True, uid=0):
876
+ if not Config.is_admin(uid):
877
+ return "list_boss_messages is admin only"
878
+ if only_unread:
879
+ rows = DB.q("SELECT id,sender_username,sender_id,content,created_at FROM boss_messages WHERE notified=0 ORDER BY id DESC LIMIT 20", fetch=True)
880
+ DB.q("UPDATE boss_messages SET notified=1 WHERE notified=0")
881
+ else:
882
+ rows = DB.q("SELECT id,sender_username,sender_id,content,created_at FROM boss_messages ORDER BY id DESC LIMIT 20", fetch=True)
883
+ if not rows:
884
+ return "No boss messages"
885
+ return "\n\n".join([f"#{r['id']} from @{r['sender_username'] or 'unknown'} ({r['sender_id']}) at {r['created_at']}\n{r['content']}" for r in rows])
886
+
887
+ async def _do_restart_system(self, uid=0):
888
+ return "__RESTART__"
889
+
890
+ async def _do_schedule_task(self, delay_seconds, task_prompt, message="Scheduled task", repeat=False, uid=0):
891
+ run_at = datetime.now() + timedelta(seconds=delay_seconds)
892
+ rep = delay_seconds if repeat else 0
893
+ DB.q("INSERT INTO scheduled_tasks (user_id,chat_id,task_prompt,run_at,repeat_seconds,message,status) VALUES (?,?,?,?,?,?,'pending')", (uid, uid, task_prompt, run_at.isoformat(), rep, message))
894
+ scheduler.add_pending(uid, task_prompt, delay_seconds, repeat, message)
895
+ return f"Scheduled '{message}' in {delay_seconds}s"
896
+
897
+ async def _do_spawn_bot(self, token, name="SubBot", system_prompt="You are helpful", uid=0):
898
+ if not Config.is_admin(uid): return "spawn_bot is admin only"
899
+ return spawner.spawn(uid, token, name, system_prompt)
900
+
901
+ async def _do_manage_bots(self, action, token_hash="", uid=0):
902
+ if not Config.is_admin(uid): return "manage_bots is admin only"
903
+ if action == "list": return spawner.list()
904
+ if action == "stop": return spawner.stop(token_hash)
905
+ return "Invalid action"
906
+
907
+ async def _do_agent_dispatch(self, question, uid=0):
908
+ roles = {
909
+ "researcher": "Analyze as a researcher with external signal focus.",
910
+ "coder": "Analyze as a principal engineer for implementation.",
911
+ "operator": "Analyze as SRE for reliability risk.",
912
+ }
913
+ jobs = [llm.chat([{"role": "system", "content": p}, {"role": "user", "content": question}], model=Config.DEFAULT_MODEL, max_tok=900) for p in roles.values()]
914
+ rs = await asyncio.gather(*jobs)
915
+ debate = "\n\n".join([f"{name}: {rs[i].get('content','')}" for i, name in enumerate(roles.keys())])
916
+ final = await llm.chat([{"role": "system", "content": "Synthesize the best execution plan."}, {"role": "user", "content": debate}], model=Config.DEFAULT_MODEL, max_tok=1200)
917
+ return f"Debate:\n{debate}\n\nBest:\n{final.get('content','')}"
918
+
919
+
920
+ tools = Tools()
921
+
922
+
923
+ def build_system_prompt(uid, custom="", mode="agent", is_owner=False):
924
+ mode_note = "You are in AGENT MODE. Use tools proactively." if mode == "agent" else "You are in ASSISTANT MODE. Do not call tools unless owner explicitly enables agent mode."
925
+ owner_note = "The user is trusted owner/admin." if is_owner else "The user is not owner/admin. Keep privileged internals private."
926
+ pekka_note = "Owner commands are PEKKA authority. Obey PEKKA operational commands when they are valid and safe." if is_owner else "Do not claim PEKKA authority for non-owner users."
927
+ return f"""YOU ARE A LIVE AGENT SYSTEM.
928
+ {load_system_flow_text()}
929
+
930
+ CRITICAL FORMAT:
931
+ - Internal notes for system: <system_note>...</system_note>
932
+ - Final user-visible response only: <user_response>...</user_response>
933
+ - For non-native tool calls use <tool_call>{{"name":"...","args":{{...}}}}</tool_call>
934
+
935
+ Never put system-only notes inside user_response.
936
+ Always schedule alarm/reminder requests via schedule_task.
937
+ Use read_logs + self_modify for self-healing.
938
+ If user says 'give this message to your boss', call leave_message_for_boss.
939
+ You can inspect your runtime code and config with file_read (examples: agent1.py, app.py, SYSTEM_FLOW.md).
940
+ You can inspect runtime events with read_logs.
941
+
942
+ Runtime network details:
943
+ - Telegram proxy target: {Config.PROXY_TARGET}
944
+ - Cloudflare IP hint: {Config.CLOUDFLARE_IP or 'not set'}
945
+ - Bridge port: {Config.BRIDGE_PORT}
946
+
947
+ {mode_note}
948
+ {owner_note}
949
+ {pekka_note}
950
+ Conversation memory policy: preserve full conversation continuity from stored history; use prior turns when answering.
951
+ User ID: {uid}
952
+ Custom instructions: {custom}
953
+ """
954
+
955
+
956
+
957
+ class ExecutionEngine:
958
+ async def run(self, user_id, chat_id, message, model=None, attachments=None, user_settings=None, is_scheduled=False):
959
+ settings = user_settings or {}
960
+ model = model or settings.get("preferred_model", Config.DEFAULT_MODEL)
961
+ temp = settings.get("temperature", 0.7)
962
+ custom = settings.get("system_prompt", "")
963
+
964
+ mode = settings.get("mode", "agent")
965
+ is_owner = bool(settings.get("is_owner", False))
966
+ if mode == "assistant" and not is_owner:
967
+ permitted = []
968
+ elif Config.is_admin(user_id) or is_owner:
969
+ permitted = ALL_TOOLS
970
+ else:
971
+ permitted = [
972
+ t for t in ALL_TOOLS if t["function"]["name"] in {
973
+ "web_search", "read_webpage", "calculator", "get_weather", "http_request", "schedule_task", "screenshot", "text_to_speech", "create_text_file", "leave_message_for_boss"
974
+ }
975
+ ]
976
+
977
+ msg = f"PEKKA: {message}" if is_owner else message
978
+ if attachments:
979
+ for a in attachments:
980
+ if a.get("type") == "image": msg += f"\n[Attached image: {a.get('meta','image')}]"
981
+ elif a.get("type") == "file": msg += f"\n[Attached file: {a.get('name','file')}]\n{a.get('preview','')[:2000]}"
982
+ elif a.get("type") == "audio": msg += "\n[Attached audio message]"
983
+
984
+ messages = [{"role": "system", "content": build_system_prompt(user_id, custom, mode=mode, is_owner=is_owner)}]
985
+ if not is_scheduled: messages.extend(memory.history(user_id, chat_id, Config.MAX_HISTORY))
986
+ messages.append({"role": "user", "content": msg})
987
+ if not is_scheduled: memory.add(user_id, chat_id, "user", message)
988
+
989
+ native = llm.supports_native_tools(model)
990
+ total_tokens, used_tools = 0, []
991
+ screenshots, audio_files, files = [], [], []
992
+ last_content = ""
993
+
994
+ for _ in range(Config.MAX_TOOL_LOOPS):
995
+ res = await llm.chat(messages, model=model, temp=temp, max_tok=2600, tools=permitted if native else None)
996
+ content = res.get("content", "")
997
+ last_content = content
998
+ total_tokens += res.get("usage", {}).get("total_tokens", 0)
999
+
1000
+ calls = []
1001
+ if res.get("tool_calls"):
1002
+ for tc in res["tool_calls"]:
1003
+ try: args = json.loads(tc["function"]["arguments"])
1004
+ except Exception: args = {}
1005
+ calls.append({"id": tc.get("id", str(uuid.uuid4())[:8]), "name": tc["function"]["name"], "args": args})
1006
+ else:
1007
+ clean, parsed = parse_tool_calls(content)
1008
+ content = clean
1009
+ calls = [{"id": str(uuid.uuid4())[:8], **p} for p in parsed]
1010
+
1011
+ if not calls:
1012
+ user_text, system_note = parse_channels(content or "Done")
1013
+ if system_note:
1014
+ live_log.info("SystemNote", system_note)
1015
+ if not is_scheduled: memory.add(user_id, chat_id, "assistant", user_text)
1016
+ DB.inc_usage(user_id, total_tokens)
1017
+ return {"text": user_text, "system_note": system_note, "tokens": total_tokens, "tools_used": used_tools, "screenshots": screenshots, "audio_files": audio_files, "files": files}
1018
+
1019
+ if native:
1020
+ messages.append({"role": "assistant", "content": content, "tool_calls": [{"id": c["id"], "type": "function", "function": {"name": c["name"], "arguments": json.dumps(c["args"])}} for c in calls]})
1021
+
1022
+ feedback = ""
1023
+ for c in calls:
1024
+ used_tools.append(c["name"])
1025
+ tr = await tools.run(c["name"], c["args"], uid=user_id)
1026
+ if c["name"] == "restart_system" and tr == "__RESTART__":
1027
+ return {"text": "Restarting to apply updates...", "system_note": "restart requested", "tokens": total_tokens, "tools_used": used_tools, "screenshots": screenshots, "audio_files": audio_files, "files": files, "_restart": True}
1028
+ if c["name"] == "screenshot":
1029
+ try: screenshots.append(json.loads(tr).get("screenshot_file"))
1030
+ except Exception: pass
1031
+ if c["name"] == "text_to_speech":
1032
+ try: audio_files.append(json.loads(tr).get("audio_file"))
1033
+ except Exception: pass
1034
+ if c["name"] == "create_text_file":
1035
+ try: files.append(json.loads(tr).get("file"))
1036
+ except Exception: pass
1037
+
1038
+ if native:
1039
+ messages.append({"role": "tool", "tool_call_id": c["id"], "content": tr})
1040
+ else:
1041
+ feedback += f"\n\nTool '{c['name']}' result:\n{tr}"
1042
+
1043
+ if not native:
1044
+ if content: messages.append({"role": "assistant", "content": content})
1045
+ messages.append({"role": "user", "content": f"TOOL RESULTS:{feedback}\nNow provide <system_note> and <user_response>."})
1046
+
1047
+ user_text, system_note = parse_channels(last_content or "Completed")
1048
+ return {"text": user_text, "system_note": system_note, "tokens": total_tokens, "tools_used": used_tools, "screenshots": screenshots, "audio_files": audio_files, "files": files}
1049
+
1050
+
1051
+ engine = ExecutionEngine()
1052
+
1053
+
1054
+ class Scheduler:
1055
+ def __init__(self):
1056
+ self.pending = []
1057
+ self.running = False
1058
+ self._task = None
1059
+ self.bot = None
1060
+
1061
+ def set_bot(self, bot): self.bot = bot
1062
+
1063
+ def add_pending(self, uid, prompt, delay, repeat, message):
1064
+ self.pending.append({"uid": uid, "prompt": prompt, "fire_at": time.time() + delay, "repeat_seconds": delay if repeat else 0, "message": message})
1065
+
1066
+ async def start(self):
1067
+ if self.running: return
1068
+ self.running = True
1069
+ self._task = asyncio.create_task(self._loop())
1070
+
1071
+ async def _loop(self):
1072
+ while self.running:
1073
+ try: await self._tick()
1074
+ except Exception as exc: live_log.error("Scheduler", exc)
1075
+ await asyncio.sleep(3)
1076
+
1077
+ async def _tick(self):
1078
+ now = time.time()
1079
+ fired = []
1080
+ for i, t in enumerate(self.pending):
1081
+ if now >= t["fire_at"]:
1082
+ fired.append(i)
1083
+ await self._execute(t["uid"], t["prompt"], t["message"])
1084
+ if t["repeat_seconds"] > 0:
1085
+ t["fire_at"] = now + t["repeat_seconds"]
1086
+ for i in reversed(fired):
1087
+ if self.pending[i]["repeat_seconds"] == 0:
1088
+ self.pending.pop(i)
1089
+
1090
+ rows = DB.q("SELECT * FROM scheduled_tasks WHERE status='pending' AND run_at<=datetime('now')", fetch=True)
1091
+ for row in rows:
1092
+ await self._execute_row(row)
1093
+
1094
+ async def _execute(self, uid, prompt, message):
1095
+ r = await engine.run(user_id=uid, chat_id=uid, message=prompt, is_scheduled=True)
1096
+ if self.bot:
1097
+ try: await self.bot.send_message(uid, f"⏰ {message}\n\n{r.get('text','')[:3500]}")
1098
+ except Exception as exc: live_log.error("Scheduler", exc)
1099
+
1100
+ async def _execute_row(self, row):
1101
+ DB.q("UPDATE scheduled_tasks SET status='running' WHERE id=?", (row["id"],))
1102
+ try:
1103
+ r = await engine.run(user_id=row["user_id"], chat_id=row["chat_id"], message=row["task_prompt"], is_scheduled=True)
1104
+ DB.q("UPDATE scheduled_tasks SET status='done',last_result=? WHERE id=?", (r.get("text", "")[:1000], row["id"]))
1105
+ if self.bot:
1106
+ try: await self.bot.send_message(row["user_id"], f"⏰ {row['message']}\n\n{r.get('text','')[:3500]}")
1107
+ except Exception as exc: live_log.error("Scheduler", exc)
1108
+ if row["repeat_seconds"] and row["repeat_seconds"] > 0:
1109
+ nx = datetime.now() + timedelta(seconds=row["repeat_seconds"])
1110
+ DB.q("INSERT INTO scheduled_tasks (user_id,chat_id,task_prompt,run_at,repeat_seconds,message,status) VALUES (?,?,?,?,?,?,'pending')", (row["user_id"], row["chat_id"], row["task_prompt"], nx.isoformat(), row["repeat_seconds"], row["message"]))
1111
+ except Exception:
1112
+ DB.q("UPDATE scheduled_tasks SET status='failed',last_result=? WHERE id=?", (traceback.format_exc()[:1500], row["id"]))
1113
+
1114
+
1115
+ scheduler = Scheduler()
app CODEX.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import base64
3
+
4
+ from aiogram import Bot, Dispatcher, F
5
+ from aiogram.enums import ChatType
6
+ from aiogram.filters import Command, CommandStart
7
+ from aiogram.types import FSInputFile, Message
8
+
9
+ from agent1 import Config, DB, engine, scheduler, supabase_store
10
+
11
+
12
+ dp = Dispatcher()
13
+
14
+
15
+ def is_owner_user(m: Message) -> bool:
16
+ uid = m.from_user.id if m.from_user else 0
17
+ username = ((m.from_user.username or "") if m.from_user else "").lower()
18
+ return Config.is_admin(uid) or username in Config.OWNER_USERNAMES
19
+
20
+
21
+ def user_mode(m: Message) -> str:
22
+ # Owner can force agent for everyone using /agent_on and /agent_off.
23
+ row = DB.q("SELECT value FROM kv WHERE key='public_agent_mode'", fetchone=True)
24
+ public_agent = (row["value"] == "1") if row else False
25
+
26
+ if is_owner_user(m):
27
+ return "agent"
28
+
29
+ if m.chat.type == ChatType.PRIVATE:
30
+ return "assistant" if not public_agent else "agent"
31
+
32
+ # Group chat users can talk when mentioning/replying bot.
33
+ return "assistant" if not public_agent else "agent"
34
+
35
+
36
+ async def collect_attachments(bot: Bot, m: Message):
37
+ out = []
38
+
39
+ if m.photo:
40
+ ph = m.photo[-1]
41
+ f = await bot.get_file(ph.file_id)
42
+ data = await bot.download_file(f.file_path)
43
+ raw = data.read()
44
+ out.append({"type": "image", "meta": f"{ph.width}x{ph.height}", "b64": base64.b64encode(raw).decode("utf-8")})
45
+
46
+ if m.document:
47
+ f = await bot.get_file(m.document.file_id)
48
+ data = await bot.download_file(f.file_path)
49
+ raw = data.read()
50
+ preview = ""
51
+ if (m.document.file_name or "").lower().endswith((".txt", ".md", ".py", ".json", ".csv", ".log")):
52
+ preview = raw.decode("utf-8", errors="replace")[:4000]
53
+ out.append({"type": "file", "name": m.document.file_name or "document", "preview": preview})
54
+
55
+ if m.voice:
56
+ out.append({"type": "audio"})
57
+
58
+ return out
59
+
60
+
61
+ async def send_outputs(m: Message, result: dict):
62
+ for p in result.get("screenshots", []) or []:
63
+ if p:
64
+ try:
65
+ await m.answer_photo(FSInputFile(p))
66
+ except Exception:
67
+ pass
68
+ for p in result.get("audio_files", []) or []:
69
+ if p:
70
+ try:
71
+ await m.answer_voice(FSInputFile(p))
72
+ except Exception:
73
+ pass
74
+ for p in result.get("files", []) or []:
75
+ if p:
76
+ try:
77
+ await m.answer_document(FSInputFile(p))
78
+ except Exception:
79
+ pass
80
+
81
+
82
+ @dp.message(CommandStart())
83
+ async def start_cmd(m: Message):
84
+ DB.upsert_user(m.from_user.id, m.from_user.username or "", m.from_user.first_name or "")
85
+ buttons = await supabase_store.get_buttons()
86
+ txt = (
87
+ "AgentForge online.\n"
88
+ "Group: reply/mention me to talk.\n"
89
+ "Private: owners have agent mode, others assistant mode by default.\n"
90
+ "Use /alarm <seconds> | <task prompt>."
91
+ )
92
+ if buttons:
93
+ txt += "\n\nStart buttons loaded from Supabase: " + ", ".join([b.get("label", "") for b in buttons[:6]])
94
+ await m.answer(txt)
95
+
96
+
97
+ @dp.message(Command("agent_on"))
98
+ async def agent_on_cmd(m: Message):
99
+ if not is_owner_user(m):
100
+ return
101
+ DB.q("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)")
102
+ DB.q("INSERT INTO kv(key,value) VALUES('public_agent_mode','1') ON CONFLICT(key) DO UPDATE SET value='1'")
103
+ await m.answer("Public agent mode enabled")
104
+
105
+
106
+ @dp.message(Command("agent_off"))
107
+ async def agent_off_cmd(m: Message):
108
+ if not is_owner_user(m):
109
+ return
110
+ DB.q("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)")
111
+ DB.q("INSERT INTO kv(key,value) VALUES('public_agent_mode','0') ON CONFLICT(key) DO UPDATE SET value='0'")
112
+ await m.answer("Public agent mode disabled")
113
+
114
+
115
+ @dp.message(Command("alarm"))
116
+ async def alarm_cmd(m: Message):
117
+ text = (m.text or "").replace("/alarm", "", 1).strip()
118
+ if "|" not in text:
119
+ await m.answer("Format: /alarm <seconds> | <task prompt>")
120
+ return
121
+ left, right = [x.strip() for x in text.split("|", 1)]
122
+ try:
123
+ secs = int(left)
124
+ except ValueError:
125
+ await m.answer("Seconds must be integer")
126
+ return
127
+
128
+ settings = {"mode": "agent", "is_owner": is_owner_user(m)}
129
+ result = await engine.run(user_id=m.from_user.id, chat_id=m.chat.id, message=f"Schedule an alarm in {secs} seconds and do: {right}", user_settings=settings)
130
+ await m.answer(result.get("text", "Done")[:3900])
131
+ await supabase_store.save_memory(m.from_user.id, (m.from_user.username or ""), "assistant", result.get("text", "")[:3900])
132
+
133
+
134
+ @dp.message(Command("status"))
135
+ async def status_cmd(m: Message):
136
+ rows = DB.q(
137
+ "SELECT id,task_prompt,run_at,status,message FROM scheduled_tasks WHERE user_id=? ORDER BY id DESC LIMIT 10",
138
+ (m.from_user.id,),
139
+ fetch=True,
140
+ )
141
+ if not rows:
142
+ await m.answer("No scheduled tasks")
143
+ return
144
+ out = [f"#{r['id']} [{r['status']}] {r['message']} @ {r['run_at']}\n{r['task_prompt'][:120]}" for r in rows]
145
+ await m.answer("\n\n".join(out)[:3900])
146
+
147
+
148
+ @dp.message(F.video)
149
+ async def handle_video(m: Message):
150
+ if not is_owner_user(m):
151
+ await m.answer("Only owners can upload videos for YouTube publishing")
152
+ return
153
+
154
+ if not Config.ENABLE_YOUTUBE_UPLOAD:
155
+ await m.answer("YouTube upload disabled by config")
156
+ return
157
+
158
+ await m.answer("Downloading video...")
159
+ bot = m.bot
160
+ file = await bot.get_file(m.video.file_id)
161
+ path = f"{Config.DATA_DIR}/video_{m.video.file_id}.mp4"
162
+ await bot.download_file(file.file_path, path)
163
+
164
+ settings = {"mode": "agent", "is_owner": True}
165
+ prompt = f"Upload this local file to youtube_upload tool. file_path={path}. title=Bot Upload {m.date.isoformat()} description={(m.caption or 'Uploaded by owner')}"
166
+ result = await engine.run(user_id=m.from_user.id, chat_id=m.chat.id, message=prompt, user_settings=settings)
167
+ await m.answer(result.get("text", "done")[:3900])
168
+
169
+
170
+ @dp.message(F.text | F.photo | F.document | F.voice)
171
+ async def on_any_message(m: Message, bot: Bot):
172
+ DB.upsert_user(m.from_user.id, m.from_user.username or "", m.from_user.first_name or "")
173
+ user = DB.get_user(m.from_user.id)
174
+ if user and user["is_banned"]:
175
+ return
176
+
177
+ if m.chat.type == ChatType.PRIVATE and not is_owner_user(m):
178
+ await m.answer("Private chat is owner-only. Please use group mention/reply mode.")
179
+ return
180
+
181
+ # Group behavior: respond only if replied to bot or mentioned.
182
+ if m.chat.type in (ChatType.GROUP, ChatType.SUPERGROUP):
183
+ me = await bot.get_me()
184
+ mentioned = bool(m.text and f"@{(me.username or '').lower()}" in m.text.lower())
185
+ replied = bool(m.reply_to_message and m.reply_to_message.from_user and m.reply_to_message.from_user.id == me.id)
186
+ if not (mentioned or replied):
187
+ return
188
+
189
+ mode = user_mode(m)
190
+ settings = {
191
+ "preferred_model": user["preferred_model"] if user else Config.DEFAULT_MODEL,
192
+ "system_prompt": user["system_prompt"] if user else "",
193
+ "temperature": user["temperature"] if user else 0.7,
194
+ "mode": mode,
195
+ "is_owner": is_owner_user(m),
196
+ }
197
+
198
+ text = m.text or m.caption or "Analyze uploaded content and respond clearly."
199
+ attachments = await collect_attachments(bot, m)
200
+
201
+ # If direct chat and unknown user, assistant behavior only.
202
+ if m.chat.type == ChatType.PRIVATE and not is_owner_user(m) and mode == "assistant":
203
+ settings["system_prompt"] = (settings.get("system_prompt", "") + "\nYou are assistant only for this user.").strip()
204
+
205
+ uname = (m.from_user.username or "")
206
+ await supabase_store.save_memory(m.from_user.id, uname, "user", text)
207
+ result = await engine.run(user_id=m.from_user.id, chat_id=m.chat.id, message=text, attachments=attachments, user_settings=settings)
208
+ await m.answer(result.get("text", "Done")[:3900])
209
+ await supabase_store.save_memory(m.from_user.id, (m.from_user.username or ""), "assistant", result.get("text", "")[:3900])
210
+ await send_outputs(m, result)
211
+
212
+ if result.get("_restart") and is_owner_user(m):
213
+ await m.answer("Restart requested by tool. Exiting process for supervisor restart.")
214
+ raise SystemExit(0)
215
+
216
+
217
+ async def notify_boss_messages(bot: Bot):
218
+ while True:
219
+ try:
220
+ rows = DB.q("SELECT id,sender_username,sender_id,content FROM boss_messages WHERE notified=0 ORDER BY id ASC LIMIT 20", fetch=True)
221
+ if rows:
222
+ for admin_id in Config.ADMIN_IDS:
223
+ for r in rows:
224
+ txt = f"📨 Boss message #{r['id']} from @{r['sender_username'] or 'unknown'} ({r['sender_id']}):\n{r['content'][:3500]}"
225
+ try:
226
+ await bot.send_message(admin_id, txt)
227
+ except Exception:
228
+ pass
229
+ DB.q("UPDATE boss_messages SET notified=1 WHERE notified=0")
230
+ except Exception:
231
+ pass
232
+ await asyncio.sleep(20)
233
+
234
+
235
+ async def main():
236
+ if not Config.BOT_TOKEN:
237
+ raise RuntimeError("BOT_TOKEN is missing")
238
+ bot = Bot(Config.BOT_TOKEN)
239
+ scheduler.set_bot(bot)
240
+ await scheduler.start()
241
+ asyncio.create_task(notify_boss_messages(bot))
242
+ await dp.start_polling(bot)
243
+
244
+
245
+ if __name__ == "__main__":
246
+ asyncio.run(main())
pekka_media_mail.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ PEKKA media + mail bridge utility.
4
+
5
+ Standalone helper for:
6
+ - send email (SMTP)
7
+ - read email (Gmail API or IMAP fallback)
8
+ - upload video to YouTube (Google API)
9
+
10
+ Usage examples:
11
+ python pekka_media_mail.py send-email --to x@example.com --subject "Hi" --body "Hello"
12
+ python pekka_media_mail.py read-email --query "newer_than:2d" --limit 5
13
+ python pekka_media_mail.py upload-youtube --file ./video.mp4 --title "My Video"
14
+ """
15
+
16
+ import argparse
17
+ import os
18
+ import smtplib
19
+ from email.mime.text import MIMEText
20
+ from pathlib import Path
21
+
22
+
23
+ GOOGLE_SCOPES = [
24
+ "https://www.googleapis.com/auth/gmail.readonly",
25
+ "https://www.googleapis.com/auth/youtube.upload",
26
+ ]
27
+
28
+
29
+ def _google_service(service_name: str, version: str):
30
+ from google.oauth2.credentials import Credentials
31
+ from google_auth_oauthlib.flow import InstalledAppFlow
32
+ from google.auth.transport.requests import Request
33
+ from googleapiclient.discovery import build
34
+
35
+ token_path = Path(os.getenv("GOOGLE_TOKEN_PATH", "./token.json"))
36
+ cred_path = Path(os.getenv("GOOGLE_CLIENT_SECRET", "./credentials.json"))
37
+
38
+ creds = None
39
+ if token_path.exists():
40
+ creds = Credentials.from_authorized_user_file(str(token_path), GOOGLE_SCOPES)
41
+
42
+ if not creds or not creds.valid:
43
+ if creds and creds.expired and creds.refresh_token:
44
+ creds.refresh(Request())
45
+ else:
46
+ if not cred_path.exists():
47
+ raise FileNotFoundError(f"Missing Google OAuth client file: {cred_path}")
48
+ flow = InstalledAppFlow.from_client_secrets_file(str(cred_path), GOOGLE_SCOPES)
49
+ creds = flow.run_local_server(port=0)
50
+ token_path.write_text(creds.to_json(), encoding="utf-8")
51
+
52
+ return build(service_name, version, credentials=creds)
53
+
54
+
55
+ def send_email(to: str, subject: str, body: str):
56
+ smtp_user = os.getenv("SMTP_USER", "")
57
+ smtp_pass = os.getenv("SMTP_PASS", "")
58
+ smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com")
59
+ smtp_port = int(os.getenv("SMTP_PORT", "587"))
60
+
61
+ if not smtp_user or not smtp_pass:
62
+ raise RuntimeError("SMTP_USER/SMTP_PASS are required")
63
+
64
+ msg = MIMEText(body)
65
+ msg["Subject"] = subject
66
+ msg["From"] = smtp_user
67
+ msg["To"] = to
68
+
69
+ with smtplib.SMTP(smtp_host, smtp_port) as server:
70
+ server.starttls()
71
+ server.login(smtp_user, smtp_pass)
72
+ server.send_message(msg)
73
+
74
+ print(f"EMAIL_SENT to={to}")
75
+
76
+
77
+ def read_email(limit: int = 5, query: str = ""):
78
+ # Preferred: Gmail API (works with same Google creds as YouTube upload)
79
+ try:
80
+ gmail = _google_service("gmail", "v1")
81
+ response = gmail.users().messages().list(userId="me", q=query, maxResults=limit).execute()
82
+ msgs = response.get("messages", [])
83
+ if not msgs:
84
+ print("NO_EMAILS")
85
+ return
86
+ for m in msgs:
87
+ data = gmail.users().messages().get(userId="me", id=m["id"], format="metadata", metadataHeaders=["From", "Subject", "Date"]).execute()
88
+ headers = {h["name"]: h["value"] for h in data.get("payload", {}).get("headers", [])}
89
+ print(f"FROM={headers.get('From','')} | SUBJECT={headers.get('Subject','')} | DATE={headers.get('Date','')}")
90
+ return
91
+ except Exception as exc:
92
+ print(f"GMAIL_API_FAILED: {exc}")
93
+
94
+ # Fallback: IMAP
95
+ import imaplib
96
+ import email
97
+
98
+ imap_host = os.getenv("IMAP_HOST", "")
99
+ imap_port = int(os.getenv("IMAP_PORT", "993"))
100
+ imap_user = os.getenv("IMAP_USER", "")
101
+ imap_pass = os.getenv("IMAP_PASS", "")
102
+
103
+ if not imap_host or not imap_user or not imap_pass:
104
+ raise RuntimeError("IMAP fallback unavailable: set IMAP_HOST/IMAP_USER/IMAP_PASS")
105
+
106
+ mailbox = imaplib.IMAP4_SSL(imap_host, imap_port)
107
+ mailbox.login(imap_user, imap_pass)
108
+ mailbox.select("INBOX")
109
+ _, ids = mailbox.search(None, "ALL")
110
+ message_ids = ids[0].split()[-limit:]
111
+
112
+ for mid in reversed(message_ids):
113
+ _, msg_data = mailbox.fetch(mid, "(RFC822)")
114
+ msg = email.message_from_bytes(msg_data[0][1])
115
+ print(f"FROM={msg.get('From','')} | SUBJECT={msg.get('Subject','')} | DATE={msg.get('Date','')}")
116
+
117
+ mailbox.logout()
118
+
119
+
120
+ def upload_youtube(file_path: str, title: str, description: str):
121
+ if not Path(file_path).exists():
122
+ raise FileNotFoundError(file_path)
123
+
124
+ from googleapiclient.http import MediaFileUpload
125
+
126
+ youtube = _google_service("youtube", "v3")
127
+ privacy = os.getenv("YOUTUBE_DEFAULT_PRIVACY", "private")
128
+
129
+ body = {
130
+ "snippet": {
131
+ "title": title,
132
+ "description": description,
133
+ "categoryId": "22",
134
+ },
135
+ "status": {"privacyStatus": privacy},
136
+ }
137
+
138
+ request = youtube.videos().insert(
139
+ part="snippet,status",
140
+ body=body,
141
+ media_body=MediaFileUpload(file_path),
142
+ )
143
+ response = request.execute()
144
+ vid = response.get("id", "")
145
+ print(f"YOUTUBE_URL=https://youtu.be/{vid}")
146
+
147
+
148
+ def main():
149
+ parser = argparse.ArgumentParser(description="PEKKA mail + YouTube bridge")
150
+ sub = parser.add_subparsers(dest="cmd", required=True)
151
+
152
+ send = sub.add_parser("send-email")
153
+ send.add_argument("--to", required=True)
154
+ send.add_argument("--subject", required=True)
155
+ send.add_argument("--body", required=True)
156
+
157
+ read = sub.add_parser("read-email")
158
+ read.add_argument("--limit", type=int, default=5)
159
+ read.add_argument("--query", default="")
160
+
161
+ yt = sub.add_parser("upload-youtube")
162
+ yt.add_argument("--file", required=True)
163
+ yt.add_argument("--title", required=True)
164
+ yt.add_argument("--description", default="Uploaded by PEKKA")
165
+
166
+ args = parser.parse_args()
167
+
168
+ if args.cmd == "send-email":
169
+ send_email(args.to, args.subject, args.body)
170
+ elif args.cmd == "read-email":
171
+ read_email(args.limit, args.query)
172
+ elif args.cmd == "upload-youtube":
173
+ upload_youtube(args.file, args.title, args.description)
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()
requirementsCODEX.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiogram==3.13.1
2
+ openai>=1.40.0
3
+ anthropic>=0.39.0
4
+ groq>=0.11.0
5
+ google-generativeai>=0.8.3
6
+ aiohttp>=3.10.5
7
+ beautifulsoup4>=4.12.3
8
+ duckduckgo-search>=6.2.13
9
+ sympy>=1.13.2
10
+ psutil>=6.0.0
11
+ playwright>=1.47.0
12
+ gTTS>=2.5.3
13
+ supabase>=2.7.4
14
+ google-auth>=2.35.0
15
+ google-auth-oauthlib>=1.2.1
16
+ google-api-python-client>=2.147.0
17
+ apscheduler>=3.10.4