hoainamgo commited on
Commit
413c22a
·
verified ·
1 Parent(s): 2d3b488

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. app.py +372 -74
  2. nous_auth_state.json +17 -8
  3. requirements.txt +16 -1
app.py CHANGED
@@ -1,52 +1,37 @@
1
  """
2
  Hermes Agent - Zero-Outbound Webhook Architecture on Hugging Face Spaces
3
  100% Reliable Inbound Telegram Webhook + Direct JSON Response + Gradio Dashboard
 
 
 
 
 
 
 
 
 
 
4
  """
5
  import os
6
  import sys
7
  import json
8
  import subprocess
 
9
  import time
 
 
10
  import psutil
 
11
  from fastapi import FastAPI, Request
12
  from fastapi.responses import JSONResponse
 
13
  import gradio as gr
14
  from dotenv import load_dotenv
15
 
16
  CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
17
  load_dotenv(os.path.join(CURRENT_DIR, ".env"))
18
 
19
- # 0. Install and load Hermes Agent runtime if not present
20
- AIAgent = None
21
- try:
22
- from run_agent import AIAgent
23
- except ImportError:
24
- try:
25
- print("[SETUP] Installing hermes-agent runtime...")
26
- subprocess.run([sys.executable, "-m", "pip", "install", "--no-deps", "hermes-agent"], check=False)
27
- from run_agent import AIAgent
28
- print("[SETUP] hermes-agent loaded successfully!")
29
- except Exception as e:
30
- print(f"[WARN] Failed loading AIAgent: {e}")
31
-
32
- # 1. Load Nous Portal OAuth state into ~/.hermes & os.environ
33
- NOUS_ACCESS_TOKEN = ""
34
- try:
35
- auth_state_path = os.path.join(CURRENT_DIR, "nous_auth_state.json")
36
- if os.path.exists(auth_state_path):
37
- from hermes_cli.auth import _write_shared_nous_state
38
- with open(auth_state_path, "r", encoding="utf-8") as f:
39
- state = json.load(f)
40
- _write_shared_nous_state(state)
41
- NOUS_ACCESS_TOKEN = state.get("access_token", "")
42
- if NOUS_ACCESS_TOKEN:
43
- os.environ["NOUS_API_KEY"] = NOUS_ACCESS_TOKEN
44
- os.environ["NOUS_JWT"] = NOUS_ACCESS_TOKEN
45
- print("[SETUP] Nous Portal OAuth state loaded successfully into environment!")
46
- except Exception as e:
47
- print(f"[SETUP WARN] Load nous auth state: {e}")
48
-
49
- # 2. Environment & Configuration
50
  BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
51
  ALLOWED_USER_ID_RAW = os.getenv("ALLOWED_USER_ID", "0").strip()
52
  try:
@@ -60,6 +45,14 @@ MODEL_NAME = os.getenv("HERMES_MODEL", "stepfun/step-3.7-flash:free").strip()
60
  START_TIME = time.time()
61
  TELEGRAM_LOGS = []
62
 
 
 
 
 
 
 
 
 
63
  def add_log(msg: str):
64
  timestamp = time.strftime("%H:%M:%S")
65
  log_entry = f"[{timestamp}] {msg}"
@@ -68,36 +61,283 @@ def add_log(msg: str):
68
  if len(TELEGRAM_LOGS) > 100:
69
  TELEGRAM_LOGS.pop(0)
70
 
71
- # 3. Hermes Agent Instance Loader
 
 
72
  agent_instance = None
 
 
 
 
 
73
 
74
- def init_hermes_agent(model_name=MODEL_NAME):
75
- global agent_instance
76
- if not AIAgent:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  try:
79
- if NOUS_ACCESS_TOKEN:
80
- os.environ["NOUS_API_KEY"] = NOUS_ACCESS_TOKEN
81
- os.environ["NOUS_JWT"] = NOUS_ACCESS_TOKEN
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
- agent_instance = AIAgent(
 
 
 
 
 
 
 
 
 
 
84
  model=model_name,
85
  provider="nous",
86
- api_key=NOUS_ACCESS_TOKEN if NOUS_ACCESS_TOKEN else None,
87
- base_url="https://inference-api.nousresearch.com/v1",
88
- quiet_mode=True
89
  )
90
- add_log(f"Hermes Agent ({model_name}) via Nous Portal initialized successfully!")
91
- return agent_instance
 
 
92
  except Exception as e:
93
  add_log(f"AIAgent Nous init error ({model_name}): {e}")
94
  return None
95
 
96
- init_hermes_agent(MODEL_NAME)
97
 
98
- # 4. Message Processing (Direct Webhook JSON Response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  def process_update(update: dict) -> dict | None:
100
- global MODEL_NAME, agent_instance
101
  msg = update.get("message")
102
  if not msg:
103
  return None
@@ -108,17 +348,16 @@ def process_update(update: dict) -> dict | None:
108
  text = (msg.get("text") or "").strip()
109
 
110
  if sender_id != ALLOWED_USER_ID:
111
- add_log(f"Blocked unauthorized message from User ID {sender_id} (@{sender.get('username')})")
112
  return None
113
 
114
  add_log(f"Processing webhook message from {sender_id}: {text[:60]}")
115
 
116
- # Helper for building Telegram JSON reply
117
- def make_reply(reply_text: str, parse_mode: str = "Markdown"):
118
  payload = {
119
  "method": "sendMessage",
120
  "chat_id": chat_id,
121
- "text": reply_text
122
  }
123
  if parse_mode:
124
  payload["parse_mode"] = parse_mode
@@ -133,6 +372,7 @@ def process_update(update: dict) -> dict | None:
133
  f"• Model mặc định: `{MODEL_NAME}` (Free 100% Nous Portal)\n"
134
  "• `/model` : Xem hoặc đổi sang các model Free khác của Nous.\n"
135
  "• `/status` : Xem Uptime, CPU, RAM và trạng thái hệ thống.\n"
 
136
  "• `/sh <lệnh>` : Chạy lệnh shell/CLI trên máy chủ.\n"
137
  "• Nhắn tin bất kỳ để chat và nhờ Hermes Agent giải quyết task."
138
  )
@@ -143,15 +383,18 @@ def process_update(update: dict) -> dict | None:
143
  uptime_sec = int(time.time() - START_TIME)
144
  uptime_str = f"{uptime_sec // 3600}h {(uptime_sec % 3600) // 60}m {uptime_sec % 60}s"
145
  mem = psutil.virtual_memory()
 
146
  stat_msg = (
147
  f"📊 *HỆ THỐNG TELEMETRY (WEBHOOK)*\n"
148
  f"━━━━━━━━━━━━━━━━━━━\n"
149
  f"⏱ *Uptime:* `{uptime_str}`\n"
150
  f"💻 *CPU Load:* `{psutil.cpu_percent()}%`\n"
151
  f"🧠 *RAM:* `{round(mem.used / 1024**2, 1)}MB / {round(mem.total / 1024**2, 1)}MB`\n"
152
- f"🌐 *Provider:* `Nous Portal (OAuth Active)`\n"
153
  f"🤖 *Active Model:* `{MODEL_NAME}`\n"
154
  f"🔑 *Authorized User ID:* `{ALLOWED_USER_ID}`\n"
 
 
155
  )
156
  return make_reply(stat_msg)
157
 
@@ -170,12 +413,22 @@ def process_update(update: dict) -> dict | None:
170
 
171
  if text.startswith("/model "):
172
  new_model = text.split(" ", 1)[1].strip()
173
- new_agent = init_hermes_agent(new_model)
 
174
  if new_agent:
175
- MODEL_NAME = new_model
176
  return make_reply(f"✅ Đã chuyển sang Model Nous: `{MODEL_NAME}` thành công!")
177
- else:
178
- return make_reply(f"⚠️ Không thể khởi tạo model `{new_model}`. Vui lòng kiểm tra lại.")
 
 
 
 
 
 
 
 
 
 
179
 
180
  # Command: /sh or /cmd
181
  if text.startswith("/sh ") or text.startswith("/cmd "):
@@ -195,63 +448,104 @@ def process_update(update: dict) -> dict | None:
195
  except Exception as e:
196
  return make_reply(f"❌ Lỗi CLI: {str(e)}")
197
 
198
- # Chat with Hermes Agent via Nous Portal
199
- if not agent_instance:
200
- init_hermes_agent(MODEL_NAME)
201
- if not agent_instance:
202
- return make_reply("⚠️ Chưa khởi tạo được Hermes Agent qua Nous Portal.")
203
 
 
204
  try:
205
- reply = agent_instance.chat(text)
206
- return make_reply(reply, parse_mode=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  except Exception as e:
208
- return make_reply(f"❌ Lỗi Hermes Agent: {str(e)}", parse_mode=None)
 
 
 
 
209
 
210
- # 5. FastAPI Application & Webhook Route
211
  app = FastAPI(title="Hermes Agent Telegram Webhook")
212
 
 
213
  @app.post("/webhook")
214
  async def telegram_webhook(request: Request):
215
  try:
216
  data = await request.json()
217
- resp_data = process_update(data)
218
- if resp_data:
219
- return JSONResponse(content=resp_data)
 
220
  except Exception as e:
221
  add_log(f"Webhook processing exception: {e}")
 
 
 
222
  return {"status": "ok"}
223
 
 
224
  @app.get("/health")
225
  def health():
226
- return {"status": "online", "model": MODEL_NAME, "user_id": ALLOWED_USER_ID}
 
 
 
 
 
 
 
 
227
 
228
- # 6. Gradio Dashboard for Space
229
  def get_system_telemetry():
230
  uptime_sec = int(time.time() - START_TIME)
231
  uptime_str = f"{uptime_sec // 3600}h {(uptime_sec % 3600) // 60}m {uptime_sec % 60}s"
232
  mem = psutil.virtual_memory()
233
  logs_str = "\n".join(TELEGRAM_LOGS[-20:]) if TELEGRAM_LOGS else "Chưa có log..."
 
234
  status_summary = (
235
  f"🟢 **STATUS: ONLINE (HF WEBHOOK MODE)**\n\n"
236
  f"• **Telegram Bot Token:** {'✅ Đã nạp' if BOT_TOKEN else '❌ Chưa nạp'}\n"
237
  f"• **Authorized User ID:** `{ALLOWED_USER_ID}`\n"
238
- f"• **Provider:** `Nous Portal (OAuth Active)`\n"
239
  f"• **Active Model:** `{MODEL_NAME}`\n"
 
 
 
240
  f"• **CPU Usage:** `{psutil.cpu_percent()}%`\n"
241
  f"• **RAM Usage:** `{round(mem.used / 1024**2, 1)}MB / {round(mem.total / 1024**2, 1)}MB`\n"
242
  f"• **Uptime:** `{uptime_str}`\n"
243
  )
244
  return status_summary, logs_str
245
 
 
246
  with gr.Blocks(title="Hermes Agent Telegram Webhook Core") as demo:
247
  gr.Markdown("# 🛡️ Hermes Agent Telegram Webhook Core (Nous Portal Free)")
248
  gr.Markdown("Hệ thống Private Telegram Webhook đang tiếp nhận tin nhắn từ Telegram và phản hồi tức thì.")
249
-
250
  with gr.Row():
251
  status_box = gr.Markdown()
252
  with gr.Row():
253
  logs_box = gr.Textbox(label="Recent Security & Webhook Logs", lines=10, max_lines=20, interactive=False)
254
-
255
  btn_refresh = gr.Button("🔄 Cập nhật trạng thái", variant="primary")
256
  btn_refresh.click(fn=get_system_telemetry, outputs=[status_box, logs_box])
257
  demo.load(fn=get_system_telemetry, outputs=[status_box, logs_box])
@@ -259,6 +553,10 @@ with gr.Blocks(title="Hermes Agent Telegram Webhook Core") as demo:
259
  # Mount Gradio onto FastAPI app
260
  app = gr.mount_gradio_app(app, demo, path="/")
261
 
 
 
 
262
  if __name__ == "__main__":
263
  import uvicorn
264
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
1
  """
2
  Hermes Agent - Zero-Outbound Webhook Architecture on Hugging Face Spaces
3
  100% Reliable Inbound Telegram Webhook + Direct JSON Response + Gradio Dashboard
4
+
5
+ Stability rules:
6
+ - Module import is lightweight: no blocking pip install, no blocking agent init.
7
+ - Heavy setup (hermes runtime install, token refresh, agent init) runs in a
8
+ background thread; /health answers immediately.
9
+ - The webhook route never blocks the asyncio event loop: all work runs in the
10
+ Starlette thread pool.
11
+ - Chat calls are capped at CHAT_BUDGET_SECONDS; if the model is slower the
12
+ webhook still answers within Telegram's window and the result is delivered
13
+ asynchronously as a fallback.
14
  """
15
  import os
16
  import sys
17
  import json
18
  import subprocess
19
+ import threading
20
  import time
21
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
22
+
23
  import psutil
24
+ import requests
25
  from fastapi import FastAPI, Request
26
  from fastapi.responses import JSONResponse
27
+ from starlette.concurrency import run_in_threadpool
28
  import gradio as gr
29
  from dotenv import load_dotenv
30
 
31
  CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
32
  load_dotenv(os.path.join(CURRENT_DIR, ".env"))
33
 
34
+ # 1. Environment & Configuration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN", "").strip()
36
  ALLOWED_USER_ID_RAW = os.getenv("ALLOWED_USER_ID", "0").strip()
37
  try:
 
45
  START_TIME = time.time()
46
  TELEGRAM_LOGS = []
47
 
48
+ AUTH_STATE_PATH = os.path.join(CURRENT_DIR, "nous_auth_state.json")
49
+ NOUS_BASE_URL = "https://inference-api.nousresearch.com/v1"
50
+ TOKEN_REFRESH_SKEW_SECONDS = 180
51
+ CHAT_BUDGET_SECONDS = 45
52
+ PROVIDER_REQUEST_TIMEOUT = 60
53
+ SETUP_START = time.time()
54
+
55
+
56
  def add_log(msg: str):
57
  timestamp = time.strftime("%H:%M:%S")
58
  log_entry = f"[{timestamp}] {msg}"
 
61
  if len(TELEGRAM_LOGS) > 100:
62
  TELEGRAM_LOGS.pop(0)
63
 
64
+
65
+ # 2. Runtime state (guarded by locks, mutated from worker threads)
66
+ AIAgent = None
67
  agent_instance = None
68
+ NOUS_ACCESS_TOKEN = ""
69
+ SETUP_STATUS = "starting"
70
+ SETUP_LOCK = threading.Lock()
71
+ CHAT_LOCK = threading.Lock()
72
+ chat_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="hermes-chat")
73
 
74
+
75
+ def get_setup_status() -> str:
76
+ if SETUP_STATUS == "ready" and agent_instance is None:
77
+ return "agent-init-failed"
78
+ return SETUP_STATUS
79
+
80
+
81
+ def _hermes_home() -> str:
82
+ return os.environ.get("HERMES_HOME") or os.path.join(os.path.expanduser("~"), ".hermes")
83
+
84
+
85
+ def ensure_provider_timeout_config(timeout_seconds: int = PROVIDER_REQUEST_TIMEOUT):
86
+ """Cap Nous request timeouts in ~/.hermes/config.yaml (hermes reads this)."""
87
+ try:
88
+ import yaml
89
+
90
+ home = _hermes_home()
91
+ os.makedirs(home, exist_ok=True)
92
+ path = os.path.join(home, "config.yaml")
93
+ cfg = {}
94
+ if os.path.exists(path):
95
+ with open(path, "r", encoding="utf-8") as f:
96
+ cfg = yaml.safe_load(f) or {}
97
+ if not isinstance(cfg, dict):
98
+ cfg = {}
99
+ providers = cfg.setdefault("providers", {})
100
+ if not isinstance(providers, dict):
101
+ providers = cfg["providers"] = {}
102
+ nous = providers.setdefault("nous", {})
103
+ if not isinstance(nous, dict):
104
+ nous = providers["nous"] = {}
105
+ if nous.get("request_timeout_seconds") == timeout_seconds:
106
+ return
107
+ nous["request_timeout_seconds"] = timeout_seconds
108
+ tmp = path + ".tmp"
109
+ with open(tmp, "w", encoding="utf-8") as f:
110
+ yaml.safe_dump(cfg, f, sort_keys=False, allow_unicode=True)
111
+ os.replace(tmp, path)
112
+ add_log(f"[SETUP] provider request timeout set to {timeout_seconds}s")
113
+ except Exception as e:
114
+ add_log(f"[SETUP WARN] provider timeout config skipped: {e}")
115
+
116
+
117
+ def load_auth_state() -> dict | None:
118
+ try:
119
+ if not os.path.exists(AUTH_STATE_PATH):
120
+ return None
121
+ with open(AUTH_STATE_PATH, "r", encoding="utf-8") as f:
122
+ state = json.load(f)
123
+ return state if isinstance(state, dict) else None
124
+ except Exception as e:
125
+ add_log(f"[SETUP WARN] read nous auth state: {e}")
126
  return None
127
+
128
+
129
+ def persist_auth_state(state: dict):
130
+ """Persist rotated OAuth state to the space file + hermes shared store."""
131
+ global NOUS_ACCESS_TOKEN
132
+ if not isinstance(state, dict):
133
+ return
134
+ try:
135
+ tmp = AUTH_STATE_PATH + ".tmp"
136
+ with open(tmp, "w", encoding="utf-8") as f:
137
+ json.dump(state, f, indent=2, ensure_ascii=False)
138
+ os.replace(tmp, AUTH_STATE_PATH)
139
+ except Exception as e:
140
+ add_log(f"[SETUP WARN] write nous auth state: {e}")
141
+ try:
142
+ from hermes_cli.auth import _write_shared_nous_state
143
+
144
+ _write_shared_nous_state(state)
145
+ except Exception:
146
+ pass
147
+ token = state.get("access_token") or ""
148
+ if token:
149
+ NOUS_ACCESS_TOKEN = token
150
+ os.environ["NOUS_API_KEY"] = token
151
+ os.environ["NOUS_JWT"] = token
152
+
153
+
154
+ def _token_needs_refresh(state: dict) -> bool:
155
+ expires_at = state.get("expires_at")
156
+ if not expires_at:
157
+ return True
158
+ try:
159
+ from datetime import datetime, timezone
160
+
161
+ exp = datetime.fromisoformat(str(expires_at).replace("Z", "+00:00"))
162
+ if exp.tzinfo is None:
163
+ exp = exp.replace(tzinfo=timezone.utc)
164
+ now = datetime.now(timezone.utc)
165
+ return (exp - now).total_seconds() < TOKEN_REFRESH_SKEW_SECONDS
166
+ except Exception:
167
+ return True
168
+
169
+
170
+ def ensure_nous_token(force: bool = False) -> tuple[bool, str]:
171
+ """Refresh the Nous OAuth access token when expired/near-expiry."""
172
+ state = load_auth_state()
173
+ if not state:
174
+ return False, "nous_auth_state.json missing"
175
+ if not force and not _token_needs_refresh(state):
176
+ if NOUS_ACCESS_TOKEN != state.get("access_token"):
177
+ persist_auth_state(state)
178
+ return True, "token valid"
179
  try:
180
+ from hermes_cli.auth import refresh_nous_oauth_from_state
181
+
182
+ new_state = refresh_nous_oauth_from_state(
183
+ state,
184
+ timeout_seconds=20,
185
+ force_refresh=force,
186
+ on_state_update=lambda s, _ev: persist_auth_state(s),
187
+ )
188
+ except Exception as e:
189
+ add_log(f"[TOKEN] refresh failed: {e}")
190
+ return False, f"refresh failed: {e}"
191
+ if not isinstance(new_state, dict) or not new_state.get("access_token"):
192
+ return False, "empty token after refresh"
193
+ if new_state != state:
194
+ persist_auth_state(new_state)
195
+ add_log("[TOKEN] access token refreshed and persisted")
196
+ elif not NOUS_ACCESS_TOKEN:
197
+ persist_auth_state(new_state)
198
+ return True, "token ready"
199
+
200
 
201
+ def init_hermes_agent(model_name: str | None = None):
202
+ global agent_instance, MODEL_NAME
203
+ if AIAgent is None:
204
+ return None
205
+ model_name = model_name or MODEL_NAME
206
+ token = NOUS_ACCESS_TOKEN
207
+ try:
208
+ if token:
209
+ os.environ["NOUS_API_KEY"] = token
210
+ os.environ["NOUS_JWT"] = token
211
+ agent = AIAgent(
212
  model=model_name,
213
  provider="nous",
214
+ api_key=token or None,
215
+ base_url=NOUS_BASE_URL,
216
+ quiet_mode=True,
217
  )
218
+ agent_instance = agent
219
+ MODEL_NAME = model_name
220
+ add_log(f"Hermes Agent ({model_name}) via Nous Portal initialized!")
221
+ return agent
222
  except Exception as e:
223
  add_log(f"AIAgent Nous init error ({model_name}): {e}")
224
  return None
225
 
 
226
 
227
+ def ensure_agent_ready() -> bool:
228
+ global agent_instance
229
+ if agent_instance is not None:
230
+ return True
231
+ if AIAgent is None:
232
+ return False
233
+ with SETUP_LOCK:
234
+ if agent_instance is not None:
235
+ return True
236
+ ok, msg = ensure_nous_token()
237
+ if not ok:
238
+ add_log(f"[AGENT] token not ready: {msg}")
239
+ return init_hermes_agent(MODEL_NAME) is not None
240
+
241
+
242
+ def _setup_worker():
243
+ """Runs off the import path: install hermes, refresh token, init agent."""
244
+ global AIAgent, SETUP_STATUS
245
+ try:
246
+ try:
247
+ from run_agent import AIAgent as _AIAgent
248
+ except ImportError:
249
+ add_log("[SETUP] hermes-agent runtime missing -> installing (background)")
250
+ try:
251
+ subprocess.run(
252
+ [
253
+ sys.executable, "-m", "pip", "install", "--no-deps",
254
+ "--timeout", "20", "--retries", "2", "hermes-agent",
255
+ ],
256
+ check=False,
257
+ timeout=420,
258
+ )
259
+ except subprocess.TimeoutExpired:
260
+ add_log("[SETUP WARN] pip install hermes-agent timed out (420s)")
261
+ from run_agent import AIAgent as _AIAgent
262
+
263
+ AIAgent = _AIAgent
264
+ add_log("[SETUP] hermes runtime loaded")
265
+ except Exception as e:
266
+ SETUP_STATUS = f"hermes-unavailable: {e}"
267
+ add_log(f"[SETUP ERROR] hermes runtime unavailable: {e}")
268
+ return
269
+
270
+ ensure_provider_timeout_config(PROVIDER_REQUEST_TIMEOUT)
271
+
272
+ state = load_auth_state()
273
+ if state:
274
+ persist_auth_state(state)
275
+ ok, msg = ensure_nous_token()
276
+ add_log(f"[SETUP] nous token: {msg} (ok={ok})")
277
+ else:
278
+ add_log("[SETUP WARN] nous_auth_state.json not found")
279
+
280
+ with SETUP_LOCK:
281
+ if agent_instance is None:
282
+ if init_hermes_agent(MODEL_NAME) is None:
283
+ SETUP_STATUS = "agent-init-failed"
284
+ add_log("[SETUP ERROR] agent init failed")
285
+ return
286
+ SETUP_STATUS = "ready"
287
+ add_log(f"[SETUP] ready in {round(time.time() - SETUP_START, 1)}s")
288
+
289
+
290
+ def _is_unauthorized(exc: Exception) -> bool:
291
+ text = str(exc).lower()
292
+ return "401" in text or "unauthorized" in text or "invalid api key" in text or "out of funds" in text
293
+
294
+
295
+ def _chat_worker(text: str) -> str:
296
+ global agent_instance
297
+ ok, msg = ensure_nous_token()
298
+ if not ok:
299
+ add_log(f"[CHAT] token issue: {msg}")
300
+ if not ensure_agent_ready():
301
+ return "⚠️ Chưa khởi tạo được Hermes Agent qua Nous Portal."
302
+ try:
303
+ return agent_instance.chat(text)
304
+ except Exception as e:
305
+ add_log(f"[CHAT] error: {e}")
306
+ if _is_unauthorized(e):
307
+ ok, msg = ensure_nous_token(force=True)
308
+ add_log(f"[CHAT] force token refresh -> {msg} (ok={ok})")
309
+ if ok and ensure_agent_ready():
310
+ try:
311
+ return agent_instance.chat(text)
312
+ except Exception as e2:
313
+ return f"❌ Lỗi Hermes Agent: {e2}"
314
+ return f"❌ Token Nous Portal không hợp lệ: {msg}"
315
+ return f"❌ Lỗi Hermes Agent: {e}"
316
+
317
+
318
+ def send_telegram_message(chat_id, text: str) -> bool:
319
+ """Async-delivery fallback used only when the webhook budget is exceeded."""
320
+ if not BOT_TOKEN:
321
+ return False
322
+ try:
323
+ if len(text) > 4000:
324
+ text = text[:4000] + "\n... [Cắt bớt do quá dài]"
325
+ resp = requests.post(
326
+ f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
327
+ json={"chat_id": chat_id, "text": text},
328
+ timeout=15,
329
+ )
330
+ if not resp.ok:
331
+ add_log(f"[TG] sendMessage failed: HTTP {resp.status_code}")
332
+ return False
333
+ return True
334
+ except Exception as e:
335
+ add_log(f"[TG] sendMessage error: {e}")
336
+ return False
337
+
338
+
339
+ # 3. Message Processing (Direct Webhook JSON Response)
340
  def process_update(update: dict) -> dict | None:
 
341
  msg = update.get("message")
342
  if not msg:
343
  return None
 
348
  text = (msg.get("text") or "").strip()
349
 
350
  if sender_id != ALLOWED_USER_ID:
351
+ add_log(f"Blocked unauthorized message from User ID {sender_id}")
352
  return None
353
 
354
  add_log(f"Processing webhook message from {sender_id}: {text[:60]}")
355
 
356
+ def make_reply(reply_text: str, parse_mode: str | None = "Markdown"):
 
357
  payload = {
358
  "method": "sendMessage",
359
  "chat_id": chat_id,
360
+ "text": reply_text,
361
  }
362
  if parse_mode:
363
  payload["parse_mode"] = parse_mode
 
372
  f"• Model mặc định: `{MODEL_NAME}` (Free 100% Nous Portal)\n"
373
  "• `/model` : Xem hoặc đổi sang các model Free khác của Nous.\n"
374
  "• `/status` : Xem Uptime, CPU, RAM và trạng thái hệ thống.\n"
375
+ "• `/login_nous` : Buộc làm mới token Nous Portal.\n"
376
  "• `/sh <lệnh>` : Chạy lệnh shell/CLI trên máy chủ.\n"
377
  "• Nhắn tin bất kỳ để chat và nhờ Hermes Agent giải quyết task."
378
  )
 
383
  uptime_sec = int(time.time() - START_TIME)
384
  uptime_str = f"{uptime_sec // 3600}h {(uptime_sec % 3600) // 60}m {uptime_sec % 60}s"
385
  mem = psutil.virtual_memory()
386
+ state = load_auth_state() or {}
387
  stat_msg = (
388
  f"📊 *HỆ THỐNG TELEMETRY (WEBHOOK)*\n"
389
  f"━━━━━━━━━━━━━━━━━━━\n"
390
  f"⏱ *Uptime:* `{uptime_str}`\n"
391
  f"💻 *CPU Load:* `{psutil.cpu_percent()}%`\n"
392
  f"🧠 *RAM:* `{round(mem.used / 1024**2, 1)}MB / {round(mem.total / 1024**2, 1)}MB`\n"
393
+ f"🌐 *Provider:* `Nous Portal (OAuth)`\n"
394
  f"🤖 *Active Model:* `{MODEL_NAME}`\n"
395
  f"🔑 *Authorized User ID:* `{ALLOWED_USER_ID}`\n"
396
+ f"⚙️ *Setup:* `{get_setup_status()}`\n"
397
+ f"🎟 *Token expires:* `{state.get('expires_at', 'unknown')}`\n"
398
  )
399
  return make_reply(stat_msg)
400
 
 
413
 
414
  if text.startswith("/model "):
415
  new_model = text.split(" ", 1)[1].strip()
416
+ with SETUP_LOCK:
417
+ new_agent = init_hermes_agent(new_model)
418
  if new_agent:
 
419
  return make_reply(f"✅ Đã chuyển sang Model Nous: `{MODEL_NAME}` thành công!")
420
+ return make_reply(f"⚠️ Không thể khởi tạo model `{new_model}`. Vui lòng kiểm tra lại.")
421
+
422
+ # Command: /login_nous - force token refresh
423
+ if text in ["/login_nous", "/refresh_token"]:
424
+ ok, msg = ensure_nous_token(force=True)
425
+ state = load_auth_state() or {}
426
+ if ok:
427
+ return make_reply(
428
+ f"✅ Token Nous Portal đã làm mới.\n`{msg}`\n"
429
+ f"🎟 Hết hạn: `{state.get('expires_at', 'unknown')}`"
430
+ )
431
+ return make_reply(f"❌ Làm mới token thất bại: `{msg}`")
432
 
433
  # Command: /sh or /cmd
434
  if text.startswith("/sh ") or text.startswith("/cmd "):
 
448
  except Exception as e:
449
  return make_reply(f"❌ Lỗi CLI: {str(e)}")
450
 
451
+ # Chat with Hermes Agent via Nous Portal (bounded wait, never hangs the bot)
452
+ if not CHAT_LOCK.acquire(blocking=False):
453
+ return make_reply("⏳ Một job khác đang chạy, gửi lại sau giây lát.")
 
 
454
 
455
+ released = False
456
  try:
457
+ future = chat_executor.submit(_chat_worker, text)
458
+ try:
459
+ reply = future.result(timeout=CHAT_BUDGET_SECONDS)
460
+ return make_reply(reply or "(Hermes trả về rỗng)", parse_mode=None)
461
+ except FutureTimeoutError:
462
+ def _deliver_later():
463
+ try:
464
+ payload = future.result()
465
+ except Exception as e:
466
+ payload = f"❌ Hermes Agent lỗi: {e}"
467
+ try:
468
+ send_telegram_message(chat_id, payload)
469
+ finally:
470
+ CHAT_LOCK.release()
471
+
472
+ threading.Thread(target=_deliver_later, daemon=True).start()
473
+ released = True
474
+ return make_reply(
475
+ f"⏳ Mô hình đang trả lời lâu hơn {CHAT_BUDGET_SECONDS}s. "
476
+ "Tôi sẽ gửi kết quả ngay khi hoàn tất."
477
+ )
478
  except Exception as e:
479
+ return make_reply(f"❌ Lỗi chat: {e}", parse_mode=None)
480
+ finally:
481
+ if not released:
482
+ CHAT_LOCK.release()
483
+
484
 
485
+ # 4. FastAPI Application & Webhook Route
486
  app = FastAPI(title="Hermes Agent Telegram Webhook")
487
 
488
+
489
  @app.post("/webhook")
490
  async def telegram_webhook(request: Request):
491
  try:
492
  data = await request.json()
493
+ except Exception:
494
+ return {"status": "ok"}
495
+ try:
496
+ resp_data = await run_in_threadpool(process_update, data)
497
  except Exception as e:
498
  add_log(f"Webhook processing exception: {e}")
499
+ resp_data = None
500
+ if resp_data:
501
+ return JSONResponse(content=resp_data)
502
  return {"status": "ok"}
503
 
504
+
505
  @app.get("/health")
506
  def health():
507
+ return {
508
+ "status": "online",
509
+ "setup": get_setup_status(),
510
+ "agent_ready": agent_instance is not None,
511
+ "model": MODEL_NAME,
512
+ "user_id": ALLOWED_USER_ID,
513
+ "uptime_seconds": int(time.time() - START_TIME),
514
+ }
515
+
516
 
517
+ # 5. Gradio Dashboard for Space
518
  def get_system_telemetry():
519
  uptime_sec = int(time.time() - START_TIME)
520
  uptime_str = f"{uptime_sec // 3600}h {(uptime_sec % 3600) // 60}m {uptime_sec % 60}s"
521
  mem = psutil.virtual_memory()
522
  logs_str = "\n".join(TELEGRAM_LOGS[-20:]) if TELEGRAM_LOGS else "Chưa có log..."
523
+ state = load_auth_state() or {}
524
  status_summary = (
525
  f"🟢 **STATUS: ONLINE (HF WEBHOOK MODE)**\n\n"
526
  f"• **Telegram Bot Token:** {'✅ Đã nạp' if BOT_TOKEN else '❌ Chưa nạp'}\n"
527
  f"• **Authorized User ID:** `{ALLOWED_USER_ID}`\n"
528
+ f"• **Provider:** `Nous Portal (OAuth)`\n"
529
  f"• **Active Model:** `{MODEL_NAME}`\n"
530
+ f"• **Setup:** `{get_setup_status()}`\n"
531
+ f"• **Agent Ready:** `{'✅' if agent_instance else '❌'}`\n"
532
+ f"• **Token expires:** `{state.get('expires_at', 'unknown')}`\n"
533
  f"• **CPU Usage:** `{psutil.cpu_percent()}%`\n"
534
  f"• **RAM Usage:** `{round(mem.used / 1024**2, 1)}MB / {round(mem.total / 1024**2, 1)}MB`\n"
535
  f"• **Uptime:** `{uptime_str}`\n"
536
  )
537
  return status_summary, logs_str
538
 
539
+
540
  with gr.Blocks(title="Hermes Agent Telegram Webhook Core") as demo:
541
  gr.Markdown("# 🛡️ Hermes Agent Telegram Webhook Core (Nous Portal Free)")
542
  gr.Markdown("Hệ thống Private Telegram Webhook đang tiếp nhận tin nhắn từ Telegram và phản hồi tức thì.")
543
+
544
  with gr.Row():
545
  status_box = gr.Markdown()
546
  with gr.Row():
547
  logs_box = gr.Textbox(label="Recent Security & Webhook Logs", lines=10, max_lines=20, interactive=False)
548
+
549
  btn_refresh = gr.Button("🔄 Cập nhật trạng thái", variant="primary")
550
  btn_refresh.click(fn=get_system_telemetry, outputs=[status_box, logs_box])
551
  demo.load(fn=get_system_telemetry, outputs=[status_box, logs_box])
 
553
  # Mount Gradio onto FastAPI app
554
  app = gr.mount_gradio_app(app, demo, path="/")
555
 
556
+ # Background setup: never blocks the import path / server start
557
+ threading.Thread(target=_setup_worker, name="hermes-setup", daemon=True).start()
558
+
559
  if __name__ == "__main__":
560
  import uvicorn
561
+
562
+ uvicorn.run(app, host="0.0.0.0", port=7860, timeout_keep_alive=15)
nous_auth_state.json CHANGED
@@ -1,13 +1,22 @@
1
  {
2
- "_schema": 1,
3
- "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InByb2QtMjAyNi0wNS0wOC0xIn0.eyJjbGllbnRfaWQiOiJoZXJtZXMtY2xpIiwib3JnX2lkIjoibmFzX29yZ2FuaXNhdGlvbjo5MzdjNjYyNC1jNzEwLTQzMTEtOTkxMy05MDg2YTFiNjZlMzMiLCJlbWFpbCI6InRlYW1zaW92bkBnbWFpbC5jb20iLCJtYW5hZ2VkX3Rvb2xzIjpmYWxzZSwic2NvcGUiOiJpbmZlcmVuY2U6aW52b2tlIiwidG9rZW5fdXNlIjoiYWNjZXNzIiwib2F1dGhfY29udHJhY3RfdmVyc2lvbiI6MSwic2lkIjoic2lkX3A4T1hVTHFwQ2xuLVQ0STctYUVpbk81LSIsInByb2R1Y3RfaWQiOiJub3VzLWhlcm1lcy1hZ2VudCIsIm5vdXNfY2xpZW50IjoiaGVybWVzLWFnZW50IiwidG9vbF9nYXRld2F5X2FkbWluIjpmYWxzZSwicGFpZF9hY2Nlc3MiOmZhbHNlLCJwcml2YWN5X21vZGUiOiJzdGFuZGFyZCIsInBvbGljeV9wcmVzZW50IjpmYWxzZSwibWVtYmVyX3NwZW5kX2NhcF9leGNlZWRlZCI6ZmFsc2UsIm1lbWJlcl9zcGVuZF9jYXBfdXNkIjpudWxsLCJtZW1iZXJfc3BlbmRfdXNkIjoiMCIsIm1lbWJlcl9zcGVuZF9jYXBfcmVtYWluaW5nX3VzZCI6bnVsbCwibWVtYmVyX2Zyb250aWVyX3NwZW5kX2NhcF9leGNlZWRlZCI6ZmFsc2UsIm1lbWJlcl9mcm9udGllcl9zcGVuZF9jYXBfdXNkIjpudWxsLCJtZW1iZXJfZnJvbnRpZXJfc3BlbmRfdXNkIjoiMCIsIm1lbWJlcl9mcm9udGllcl9zcGVuZF9jYXBfcmVtYWluaW5nX3VzZCI6bnVsbCwidG9vbF9hY2Nlc3MiOnsiZW5hYmxlZCI6ZmFsc2UsImNvdmVyYWdlIjp7ImZpcmVjcmF3bCI6dHJ1ZSwiZmFsIjp0cnVlLCJmYWwtdmlkZW8iOmZhbHNlLCJvcGVuYWktYXVkaW8iOnRydWUsImJyb3dzZXItdXNlIjp0cnVlLCJtb2RhbCI6dHJ1ZSwia3JlYSI6ZmFsc2V9fSwicmF0ZV9saW1pdF9wcm9maWxlX2lkIjotMTIsInJhdGVfbGltaXRfcnBtIjo1MCwicmF0ZV9saW1pdF90cG0iOjUwMDAwMCwicmF0ZV9saW1pdF9ycGgiOjIxMDAsInJhdGVfbGltaXRfdHBoIjo2MDAwMDAwLCJyYXRlX2xpbWl0X3NvdXJjZSI6ImZyZWVfaGVybWVzX2FnZW50Iiwic3ViIjoibmFzX3VzZXI6ZTc1OTc3YTktMDRjYy00YmYzLTk0MTMtNzM0YTIxMmZhZjQ0IiwiaXNzIjoiaHR0cHM6Ly9wb3J0YWwubm91c3Jlc2VhcmNoLmNvbSIsImF1ZCI6Imhlcm1lcy1jbGk6aGVybWVzLWNsaSIsImlhdCI6MTc5MTI1ODk3MCwiZXhwIjoxNzkxMjYyNTcwLCJqdGkiOiJxQ0lfaGZCTTNaUnpVYlM1QWI1WnNRIn0.zfGtBG8gA-TmA_Gsy7LTZ6ZMUmP83jquHgUwMhY3reOsavn4yCOMC-5vYTKcS0WOilf9gnrQP66TMa0UKPheRmUyFRobsN4AecjuXKs9QYf6DkqqVEa3WD-ZF_76l3VYewfRwFaBdfPdRsQDmRR67bpzRBlfclPRouAap5D7MnYySsO1hfe8OdM2D5euduN5EfmIvCtvWsIcBxsEO9pb9Xt-tGnY4kI0EsXvsaMUmEEcvu3W4bLilRcd6bCskxs2PLb9KQKGXjrBf0Sr0MzYKFpbr1fezq44MQmhFo99vLBhKSZapuPxX0S9eMuH02lQHZGb-ZspkMn60Cpjc1zI0A",
4
  "client_id": "hermes-cli",
5
- "expires_at": "2026-10-06T04:56:10+00:00",
6
- "inference_base_url": "https://inference-api.nousresearch.com/v1",
7
- "obtained_at": null,
8
  "portal_base_url": "https://portal.nousresearch.com",
9
- "refresh_token": "rt_QDXhzkEOLxLhf3mYp4NnbJgIrzR3RQ5SEeWJdq6EvSQuhunPt_ADzHqFiQxAvL0N",
10
- "scope": "inference:invoke",
11
  "token_type": "Bearer",
12
- "updated_at": "2026-10-06T03:56:11.189668+00:00"
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
 
1
  {
2
+ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InByb2QtMjAyNi0wNS0wOC0xIn0.eyJjbGllbnRfaWQiOiJoZXJtZXMtY2xpIiwib3JnX2lkIjoibmFzX29yZ2FuaXNhdGlvbjo5MzdjNjYyNC1jNzEwLTQzMTEtOTkxMy05MDg2YTFiNjZlMzMiLCJlbWFpbCI6InRlYW1zaW92bkBnbWFpbC5jb20iLCJtYW5hZ2VkX3Rvb2xzIjpmYWxzZSwic2NvcGUiOiJpbmZlcmVuY2U6aW52b2tlIiwidG9rZW5fdXNlIjoiYWNjZXNzIiwib2F1dGhfY29udHJhY3RfdmVyc2lvbiI6MSwic2lkIjoic2lkX3A4T1hVTHFwQ2xuLVQ0STctYUVpbk81LSIsInByb2R1Y3RfaWQiOiJub3VzLWhlcm1lcy1hZ2VudCIsIm5vdXNfY2xpZW50IjoiaGVybWVzLWFnZW50IiwidG9vbF9nYXRld2F5X2FkbWluIjpmYWxzZSwicGFpZF9hY2Nlc3MiOmZhbHNlLCJwcml2YWN5X21vZGUiOiJzdGFuZGFyZCIsInBvbGljeV9wcmVzZW50IjpmYWxzZSwibWVtYmVyX3NwZW5kX2NhcF9leGNlZWRlZCI6ZmFsc2UsIm1lbWJlcl9zcGVuZF9jYXBfdXNkIjpudWxsLCJtZW1iZXJfc3BlbmRfdXNkIjoiMCIsIm1lbWJlcl9zcGVuZF9jYXBfcmVtYWluaW5nX3VzZCI6bnVsbCwibWVtYmVyX2Zyb250aWVyX3NwZW5kX2NhcF9leGNlZWRlZCI6ZmFsc2UsIm1lbWJlcl9mcm9udGllcl9zcGVuZF9jYXBfdXNkIjpudWxsLCJtZW1iZXJfZnJvbnRpZXJfc3BlbmRfdXNkIjoiMCIsIm1lbWJlcl9mcm9udGllcl9zcGVuZF9jYXBfcmVtYWluaW5nX3VzZCI6bnVsbCwidG9vbF9hY2Nlc3MiOnsiZW5hYmxlZCI6ZmFsc2UsImNvdmVyYWdlIjp7ImZpcmVjcmF3bCI6dHJ1ZSwiZmFsIjp0cnVlLCJmYWwtdmlkZW8iOmZhbHNlLCJvcGVuYWktYXVkaW8iOnRydWUsImJyb3dzZXItdXNlIjp0cnVlLCJtb2RhbCI6dHJ1ZSwia3JlYSI6ZmFsc2V9fSwicmF0ZV9saW1pdF9wcm9maWxlX2lkIjotMTIsInJhdGVfbGltaXRfcnBtIjo1MCwicmF0ZV9saW1pdF90cG0iOjUwMDAwMCwicmF0ZV9saW1pdF9ycGgiOjIxMDAsInJhdGVfbGltaXRfdHBoIjo2MDAwMDAwLCJyYXRlX2xpbWl0X3NvdXJjZSI6ImZyZWVfaGVybWVzX2FnZW50Iiwic3ViIjoibmFzX3VzZXI6ZTc1OTc3YTktMDRjYy00YmYzLTk0MTMtNzM0YTIxMmZhZjQ0IiwiaXNzIjoiaHR0cHM6Ly9wb3J0YWwubm91c3Jlc2VhcmNoLmNvbSIsImF1ZCI6Imhlcm1lcy1jbGk6aGVybWVzLWNsaSIsImlhdCI6MTc5MTI3MzYxNiwiZXhwIjoxNzkxMjc3MjE2LCJqdGkiOiJid29GMFROdGxSSHNXLVVPRDcwQ3pnIn0.wkEpdoQnKADOjDW5Zwj_yLRml_8WnDW-WrZn4FHDrx37o6H6ujm5QtN6OwFkjqFxbBWEI1y_4QfubZFdqRzl7f-ZvKllE1kMxLYsavo-Hhe8H2vfiG8dDNWIBFIKZ0PX_QT5JAiQQPatGpnBsdFING9AXDICiX-c6_a7Gt9KWDvBiqRCh0RHS6BUqCv97mgLqCC8WLJ99gEZ3zdoprITjHsanG-KV_FSUf4zlWpQs6_bBPTB1WvZvlseAmB02va5HLh9N-0CThOmDTQRVJp32KxYTcKyp-tt-5WDABzhvufBE6syCARirBswEo3yes3_BbQoInJSlJvrSyye3u5Ivw",
3
+ "refresh_token": "rt_gDn0-mNF0bY3aW86OpBSAakUcqWov-ORZd_X9gJCP4zCyqq6IEiPxZFIwDGUTSit",
4
  "client_id": "hermes-cli",
 
 
 
5
  "portal_base_url": "https://portal.nousresearch.com",
6
+ "inference_base_url": "https://inference-api.nousresearch.com/v1",
 
7
  "token_type": "Bearer",
8
+ "scope": "inference:invoke",
9
+ "obtained_at": "2026-10-06T08:00:16.364298+00:00",
10
+ "expires_at": "2026-10-06T09:00:16+00:00",
11
+ "agent_key": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InByb2QtMjAyNi0wNS0wOC0xIn0.eyJjbGllbnRfaWQiOiJoZXJtZXMtY2xpIiwib3JnX2lkIjoibmFzX29yZ2FuaXNhdGlvbjo5MzdjNjYyNC1jNzEwLTQzMTEtOTkxMy05MDg2YTFiNjZlMzMiLCJlbWFpbCI6InRlYW1zaW92bkBnbWFpbC5jb20iLCJtYW5hZ2VkX3Rvb2xzIjpmYWxzZSwic2NvcGUiOiJpbmZlcmVuY2U6aW52b2tlIiwidG9rZW5fdXNlIjoiYWNjZXNzIiwib2F1dGhfY29udHJhY3RfdmVyc2lvbiI6MSwic2lkIjoic2lkX3A4T1hVTHFwQ2xuLVQ0STctYUVpbk81LSIsInByb2R1Y3RfaWQiOiJub3VzLWhlcm1lcy1hZ2VudCIsIm5vdXNfY2xpZW50IjoiaGVybWVzLWFnZW50IiwidG9vbF9nYXRld2F5X2FkbWluIjpmYWxzZSwicGFpZF9hY2Nlc3MiOmZhbHNlLCJwcml2YWN5X21vZGUiOiJzdGFuZGFyZCIsInBvbGljeV9wcmVzZW50IjpmYWxzZSwibWVtYmVyX3NwZW5kX2NhcF9leGNlZWRlZCI6ZmFsc2UsIm1lbWJlcl9zcGVuZF9jYXBfdXNkIjpudWxsLCJtZW1iZXJfc3BlbmRfdXNkIjoiMCIsIm1lbWJlcl9zcGVuZF9jYXBfcmVtYWluaW5nX3VzZCI6bnVsbCwibWVtYmVyX2Zyb250aWVyX3NwZW5kX2NhcF9leGNlZWRlZCI6ZmFsc2UsIm1lbWJlcl9mcm9udGllcl9zcGVuZF9jYXBfdXNkIjpudWxsLCJtZW1iZXJfZnJvbnRpZXJfc3BlbmRfdXNkIjoiMCIsIm1lbWJlcl9mcm9udGllcl9zcGVuZF9jYXBfcmVtYWluaW5nX3VzZCI6bnVsbCwidG9vbF9hY2Nlc3MiOnsiZW5hYmxlZCI6ZmFsc2UsImNvdmVyYWdlIjp7ImZpcmVjcmF3bCI6dHJ1ZSwiZmFsIjp0cnVlLCJmYWwtdmlkZW8iOmZhbHNlLCJvcGVuYWktYXVkaW8iOnRydWUsImJyb3dzZXItdXNlIjp0cnVlLCJtb2RhbCI6dHJ1ZSwia3JlYSI6ZmFsc2V9fSwicmF0ZV9saW1pdF9wcm9maWxlX2lkIjotMTIsInJhdGVfbGltaXRfcnBtIjo1MCwicmF0ZV9saW1pdF90cG0iOjUwMDAwMCwicmF0ZV9saW1pdF9ycGgiOjIxMDAsInJhdGVfbGltaXRfdHBoIjo2MDAwMDAwLCJyYXRlX2xpbWl0X3NvdXJjZSI6ImZyZWVfaGVybWVzX2FnZW50Iiwic3ViIjoibmFzX3VzZXI6ZTc1OTc3YTktMDRjYy00YmYzLTk0MTMtNzM0YTIxMmZhZjQ0IiwiaXNzIjoiaHR0cHM6Ly9wb3J0YWwubm91c3Jlc2VhcmNoLmNvbSIsImF1ZCI6Imhlcm1lcy1jbGk6aGVybWVzLWNsaSIsImlhdCI6MTc5MTI3MzYxNiwiZXhwIjoxNzkxMjc3MjE2LCJqdGkiOiJid29GMFROdGxSSHNXLVVPRDcwQ3pnIn0.wkEpdoQnKADOjDW5Zwj_yLRml_8WnDW-WrZn4FHDrx37o6H6ujm5QtN6OwFkjqFxbBWEI1y_4QfubZFdqRzl7f-ZvKllE1kMxLYsavo-Hhe8H2vfiG8dDNWIBFIKZ0PX_QT5JAiQQPatGpnBsdFING9AXDICiX-c6_a7Gt9KWDvBiqRCh0RHS6BUqCv97mgLqCC8WLJ99gEZ3zdoprITjHsanG-KV_FSUf4zlWpQs6_bBPTB1WvZvlseAmB02va5HLh9N-0CThOmDTQRVJp32KxYTcKyp-tt-5WDABzhvufBE6syCARirBswEo3yes3_BbQoInJSlJvrSyye3u5Ivw",
12
+ "agent_key_expires_at": "2026-10-06T09:00:16+00:00",
13
+ "tls": {
14
+ "insecure": false,
15
+ "ca_bundle": null
16
+ },
17
+ "expires_in": 3599,
18
+ "agent_key_id": null,
19
+ "agent_key_expires_in": 3599,
20
+ "agent_key_reused": false,
21
+ "agent_key_obtained_at": "2026-10-06T08:00:16.394580+00:00"
22
  }
requirements.txt CHANGED
@@ -2,9 +2,24 @@ python-telegram-bot>=21.0
2
  requests
3
  psutil
4
  python-dotenv
5
- openai
6
  croniter
7
  pathspec
8
  rich
9
  ruamel.yaml
10
  fire
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  requests
3
  psutil
4
  python-dotenv
5
+ openai==2.24.0
6
  croniter
7
  pathspec
8
  rich
9
  ruamel.yaml
10
  fire
11
+ # hermes-agent runtime deps (hermes itself is installed with --no-deps at
12
+ # runtime because its Pillow==12.2.0 pin conflicts with gradio 5.x)
13
+ prompt_toolkit
14
+ Markdown
15
+ PyJWT[crypto]
16
+ cryptography
17
+ websockets
18
+ pyyaml
19
+ tenacity
20
+ httpx[socks]
21
+ jinja2
22
+ packaging
23
+ python-multipart
24
+ ptyprocess; sys_platform != "win32"
25
+ concurrent-log-handler; sys_platform == "win32"