naldo commited on
Commit
72cca28
·
1 Parent(s): a44c2ed

Run Gemini bot as full Voyager agent

Browse files
gemini_bot_stack.py CHANGED
@@ -1,10 +1,11 @@
1
- """Independent Gemini CLI + Mineflayer player; no Voyager checkpoint changes."""
2
  from __future__ import annotations
3
 
4
  import json
5
  import os
6
  import re
7
  import subprocess
 
8
  import threading
9
  import time
10
  import urllib.request
@@ -14,9 +15,17 @@ from typing import Any
14
  from voyager_stack import ROOT as VOYAGER_ROOT, SRC as VOYAGER_SRC, _wait_tcp
15
 
16
  ROOT = Path(os.environ.get("GEMINI_BOT_RUNTIME_HOME", "/tmp/gemini-bot-runtime"))
17
- LOG_PATH = ROOT / "bot.log"
 
 
 
 
 
 
18
  CLI_VERSION = "0.61.0"
19
  MODEL = "gemini-3.5-flash-lite"
 
 
20
  KEY_NAMES = tuple(f"GEMINI_BOT_KEY_{i}" for i in range(1, 7))
21
 
22
 
@@ -26,11 +35,31 @@ def _node_version() -> int:
26
  return int(match.group(1)) if match else 0
27
 
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  class GeminiBotStack:
30
  def __init__(self) -> None:
31
  self.lock = threading.RLock()
32
  self.worker: threading.Thread | None = None
33
  self.proc: subprocess.Popen[str] | None = None
 
34
  self.stop_event = threading.Event()
35
  self.state = "stopped"
36
  self.error = ""
@@ -41,60 +70,156 @@ class GeminiBotStack:
41
  ROOT.mkdir(parents=True, exist_ok=True)
42
  if _node_version() < 20:
43
  raise RuntimeError("Gemini CLI 0.61.0 needs Node.js 20 or newer")
 
44
  node_modules = VOYAGER_SRC / "voyager" / "env" / "mineflayer" / "node_modules"
45
  marker = VOYAGER_ROOT / ".node-ready"
46
  deadline = time.monotonic() + 900
47
- while not self.stop_event.is_set() and (not marker.exists() or not (node_modules / "mineflayer").exists()):
 
 
48
  if time.monotonic() > deadline:
49
  raise RuntimeError("Voyager Mineflayer dependencies did not become ready")
50
  self.stop_event.wait(2)
51
  if self.stop_event.is_set():
52
  raise RuntimeError("stopped")
 
53
  cli_dir = ROOT / "cli"
54
  cli = cli_dir / "node_modules" / ".bin" / "gemini"
55
  version_marker = cli_dir / ".installed-version"
56
- if not cli.exists() or not version_marker.exists() or version_marker.read_text().strip() != CLI_VERSION:
 
 
 
 
57
  cli_dir.mkdir(parents=True, exist_ok=True)
58
  result = subprocess.run(
59
- ["npm", "install", "--no-audit", "--no-fund", "--prefix", str(cli_dir),
60
- f"@google/gemini-cli@{CLI_VERSION}"], cwd=ROOT,
61
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=600, check=False,
 
 
 
 
 
 
 
62
  )
63
  if result.returncode or not cli.exists():
64
  raise RuntimeError(f"Gemini CLI install failed (rc={result.returncode})")
65
  version_marker.write_text(CLI_VERSION + "\n", encoding="utf-8")
 
66
  workspace = ROOT / "workspace"
67
  settings = workspace / ".gemini" / "settings.json"
68
  settings.parent.mkdir(parents=True, exist_ok=True)
69
- # The CLI is a planning-only model client. It cannot modify files or
70
- # call terminal tools; the Mineflayer side validates each game action.
71
  settings.write_text(json.dumps({
72
  "general": {"enableAutoUpdate": False, "plan": {"modelRouting": False}},
73
  "tools": {"core": [], "exclude": [
74
  "run_shell_command", "write_file", "replace", "edit_file", "web_fetch",
75
- "google_web_search", "read_file", "read_many_files", "glob", "grep", "list_directory",
 
76
  ]},
77
  "model": {"maxSessionTurns": 1},
78
  "telemetry": {"enabled": False, "logPrompts": False},
79
  "privacy": {"usageStatisticsEnabled": False},
80
  }), encoding="utf-8")
81
  (settings.parent / "system.md").write_text(
82
- "You are the planning model for Gemini_Bot in Minecraft Creative mode. "
83
- "Return only one JSON action requested by the user prompt. "
84
- "Do not use tools, run commands, read files, or reveal credentials.\n",
 
85
  encoding="utf-8",
86
  )
87
  home = ROOT / "home"
88
  home.mkdir(parents=True, exist_ok=True)
 
89
  return cli, workspace
90
 
91
- def _pump(self, proc: subprocess.Popen[str]) -> None:
92
  assert proc.stdout is not None
93
- with LOG_PATH.open("a", encoding="utf-8", errors="replace") as log:
94
  for line in proc.stdout:
95
  log.write(line)
96
  log.flush()
97
- print("[gemini-bot] " + line.rstrip(), flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
  def _boot(self) -> None:
100
  self.started_at = time.time()
@@ -104,105 +229,159 @@ class GeminiBotStack:
104
  self.error = ""
105
  if not _wait_tcp("127.0.0.1", 25565, 240):
106
  raise RuntimeError("Minecraft Java is unavailable")
107
- ready_file = Path(os.environ.get("MINECRAFT_READY_FILE", "/tmp/minecraft-stack/minecraft.ready"))
 
 
 
108
  deadline = time.monotonic() + 240
109
- while not self.stop_event.is_set() and not ready_file.exists() and time.monotonic() < deadline:
 
 
 
 
110
  self.stop_event.wait(1)
111
  if not ready_file.exists():
112
  raise RuntimeError("Minecraft world did not finish loading")
 
113
  if not any(os.environ.get(name) for name in KEY_NAMES):
114
  with self.lock:
115
  self.state = "waiting-credentials"
116
  self.error = "Set GEMINI_BOT_KEY_1 as a Space secret"
117
  return
 
118
  with self.lock:
119
  self.state = "preparing"
120
  cli, workspace = self._prepare()
121
- env = {name: os.environ[name] for name in KEY_NAMES if os.environ.get(name)}
122
- env.update({
123
- "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
124
- "HOME": str(ROOT / "home"),
125
- "NODE_PATH": str(VOYAGER_SRC / "voyager" / "env" / "mineflayer" / "node_modules"),
126
- "GEMINI_BOT_CLI": str(cli),
127
- "GEMINI_BOT_WORKSPACE": str(workspace),
128
- "GEMINI_BOT_HOME": str(ROOT / "home"),
129
- "GEMINI_BOT_SYSTEM_MD": str(workspace / ".gemini" / "system.md"),
130
- "GEMINI_BOT_STATUS_PORT": "3001",
131
- })
132
- entrypoint = Path(__file__).resolve().parent / "gemini_bot.js"
133
  backoff = 5
 
134
  while not self.stop_event.is_set():
135
- self.proc = subprocess.Popen(["node", str(entrypoint)], cwd=str(workspace), env=env,
136
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
137
- text=True, bufsize=1)
138
- threading.Thread(target=self._pump, args=(self.proc,), daemon=True).start()
139
  time.sleep(3)
140
- if self.proc.poll() is None:
141
  with self.lock:
142
  self.state = "running"
143
  self.error = ""
144
- rc = self.proc.wait()
145
- else:
146
- rc = self.proc.returncode
 
 
 
 
 
147
  if self.stop_event.is_set():
148
  break
 
 
 
 
 
149
  self.restart_count += 1
150
  with self.lock:
151
  self.state = "restarting"
152
- self.error = f"Gemini bot exited rc={rc}; retry {self.restart_count}"
 
 
 
153
  if self.stop_event.wait(backoff):
154
  break
155
  backoff = min(backoff * 2, 120)
 
 
 
156
  with self.lock:
157
  self.state = "stopped"
158
  except Exception as exc:
 
 
159
  with self.lock:
160
  self.state = "error"
161
- self.error = str(exc)[:160]
162
- print(f"[gemini-bot] startup error: {type(exc).__name__}: {self.error}", flush=True)
 
 
 
163
 
164
  def start_async(self) -> dict[str, Any]:
165
  with self.lock:
166
  if self.worker and self.worker.is_alive():
167
  return self.status()
168
  self.stop_event.clear()
169
- self.worker = threading.Thread(target=self._boot, daemon=True, name="gemini-bot-bootstrap")
 
 
170
  self.worker.start()
171
  return self.status()
172
 
173
  def stop(self) -> dict[str, Any]:
174
  self.stop_event.set()
175
  with self.lock:
176
- proc = self.proc
 
177
  self.state = "stopped"
178
- if proc and proc.poll() is None:
179
- proc.terminate()
180
- try:
181
- proc.wait(timeout=15)
182
- except subprocess.TimeoutExpired:
183
- proc.kill()
184
  return self.status()
185
 
186
- def status(self) -> dict[str, Any]:
187
  try:
188
- with urllib.request.urlopen("http://127.0.0.1:3001/health", timeout=1) as response:
189
- game = json.load(response)
190
- except (OSError, ValueError):
191
- game = {"ready": False}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  with self.lock:
193
  proc = self.proc
 
 
 
194
  return {
195
  "state": self.state,
196
  "error": self.error or None,
197
- "running": bool(proc and proc.poll() is None),
198
- "pid": proc.pid if proc and proc.poll() is None else None,
 
199
  "restart_count": self.restart_count,
200
  "started_at": self.started_at,
 
 
201
  "bot_username": "Gemini_Bot",
202
  "model": MODEL,
203
  "cli_version": CLI_VERSION,
204
- "configured_key_count": sum(bool(os.environ.get(name)) for name in KEY_NAMES),
205
- "game": game,
 
 
 
 
 
 
 
 
 
 
 
 
206
  }
207
 
208
 
 
1
+ """Second full Voyager agent backed by Gemini CLI, isolated from Naldo_Bot."""
2
  from __future__ import annotations
3
 
4
  import json
5
  import os
6
  import re
7
  import subprocess
8
+ import sys
9
  import threading
10
  import time
11
  import urllib.request
 
15
  from voyager_stack import ROOT as VOYAGER_ROOT, SRC as VOYAGER_SRC, _wait_tcp
16
 
17
  ROOT = Path(os.environ.get("GEMINI_BOT_RUNTIME_HOME", "/tmp/gemini-bot-runtime"))
18
+ PERSIST_ROOT = Path(os.environ.get("MINECRAFT_PERSIST_HOME", "/persist/minecraft-friends"))
19
+ CKPT_DIR = Path(os.environ.get(
20
+ "GEMINI_VOYAGER_CKPT",
21
+ str(PERSIST_ROOT / "gemini-voyager" / "ckpt-20260927-surface-v1"),
22
+ ))
23
+ LOG_PATH = ROOT / "voyager.log"
24
+ PROXY_LOG_PATH = ROOT / "proxy.log"
25
  CLI_VERSION = "0.61.0"
26
  MODEL = "gemini-3.5-flash-lite"
27
+ PROXY_PORT = int(os.environ.get("GEMINI_VOYAGER_PROXY_PORT", "7863"))
28
+ MINEFLAYER_PORT = int(os.environ.get("GEMINI_VOYAGER_MINEFLAYER_PORT", "3001"))
29
  KEY_NAMES = tuple(f"GEMINI_BOT_KEY_{i}" for i in range(1, 7))
30
 
31
 
 
35
  return int(match.group(1)) if match else 0
36
 
37
 
38
+ def _terminate(proc: subprocess.Popen[str] | None, timeout: int = 15) -> None:
39
+ if not proc or proc.poll() is not None:
40
+ return
41
+ proc.terminate()
42
+ try:
43
+ proc.wait(timeout=timeout)
44
+ except subprocess.TimeoutExpired:
45
+ proc.kill()
46
+ proc.wait(timeout=5)
47
+
48
+
49
+ def _read_json(path: Path) -> list[str]:
50
+ try:
51
+ value = json.loads(path.read_text(encoding="utf-8"))
52
+ return value if isinstance(value, list) else []
53
+ except (OSError, ValueError):
54
+ return []
55
+
56
+
57
  class GeminiBotStack:
58
  def __init__(self) -> None:
59
  self.lock = threading.RLock()
60
  self.worker: threading.Thread | None = None
61
  self.proc: subprocess.Popen[str] | None = None
62
+ self.proxy_proc: subprocess.Popen[str] | None = None
63
  self.stop_event = threading.Event()
64
  self.state = "stopped"
65
  self.error = ""
 
70
  ROOT.mkdir(parents=True, exist_ok=True)
71
  if _node_version() < 20:
72
  raise RuntimeError("Gemini CLI 0.61.0 needs Node.js 20 or newer")
73
+
74
  node_modules = VOYAGER_SRC / "voyager" / "env" / "mineflayer" / "node_modules"
75
  marker = VOYAGER_ROOT / ".node-ready"
76
  deadline = time.monotonic() + 900
77
+ while not self.stop_event.is_set() and (
78
+ not marker.exists() or not (node_modules / "mineflayer").exists()
79
+ ):
80
  if time.monotonic() > deadline:
81
  raise RuntimeError("Voyager Mineflayer dependencies did not become ready")
82
  self.stop_event.wait(2)
83
  if self.stop_event.is_set():
84
  raise RuntimeError("stopped")
85
+
86
  cli_dir = ROOT / "cli"
87
  cli = cli_dir / "node_modules" / ".bin" / "gemini"
88
  version_marker = cli_dir / ".installed-version"
89
+ if (
90
+ not cli.exists()
91
+ or not version_marker.exists()
92
+ or version_marker.read_text().strip() != CLI_VERSION
93
+ ):
94
  cli_dir.mkdir(parents=True, exist_ok=True)
95
  result = subprocess.run(
96
+ [
97
+ "npm", "install", "--no-audit", "--no-fund", "--prefix", str(cli_dir),
98
+ f"@google/gemini-cli@{CLI_VERSION}",
99
+ ],
100
+ cwd=ROOT,
101
+ stdout=subprocess.PIPE,
102
+ stderr=subprocess.STDOUT,
103
+ text=True,
104
+ timeout=600,
105
+ check=False,
106
  )
107
  if result.returncode or not cli.exists():
108
  raise RuntimeError(f"Gemini CLI install failed (rc={result.returncode})")
109
  version_marker.write_text(CLI_VERSION + "\n", encoding="utf-8")
110
+
111
  workspace = ROOT / "workspace"
112
  settings = workspace / ".gemini" / "settings.json"
113
  settings.parent.mkdir(parents=True, exist_ok=True)
 
 
114
  settings.write_text(json.dumps({
115
  "general": {"enableAutoUpdate": False, "plan": {"modelRouting": False}},
116
  "tools": {"core": [], "exclude": [
117
  "run_shell_command", "write_file", "replace", "edit_file", "web_fetch",
118
+ "google_web_search", "read_file", "read_many_files", "glob", "grep",
119
+ "list_directory",
120
  ]},
121
  "model": {"maxSessionTurns": 1},
122
  "telemetry": {"enabled": False, "logPrompts": False},
123
  "privacy": {"usageStatisticsEnabled": False},
124
  }), encoding="utf-8")
125
  (settings.parent / "system.md").write_text(
126
+ "You are the language model inside the Voyager Minecraft agent. "
127
+ "Follow the supplied Voyager system and user prompts exactly. "
128
+ "Return only the format requested by the current prompt. "
129
+ "Do not use Gemini CLI tools, run commands, read files, or reveal credentials.\n",
130
  encoding="utf-8",
131
  )
132
  home = ROOT / "home"
133
  home.mkdir(parents=True, exist_ok=True)
134
+ CKPT_DIR.mkdir(parents=True, exist_ok=True)
135
  return cli, workspace
136
 
137
+ def _pump(self, proc: subprocess.Popen[str], path: Path, label: str) -> None:
138
  assert proc.stdout is not None
139
+ with path.open("a", encoding="utf-8", errors="replace") as log:
140
  for line in proc.stdout:
141
  log.write(line)
142
  log.flush()
143
+ print(f"[{label}] " + line.rstrip(), flush=True)
144
+
145
+ def _child_env(self, cli: Path, workspace: Path) -> dict[str, str]:
146
+ env = os.environ.copy()
147
+ env.update({
148
+ "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
149
+ "HOME": str(ROOT / "home"),
150
+ "NODE_PATH": str(
151
+ VOYAGER_SRC / "voyager" / "env" / "mineflayer" / "node_modules"
152
+ ),
153
+ "GEMINI_BOT_CLI": str(cli),
154
+ "GEMINI_BOT_WORKSPACE": str(workspace),
155
+ "GEMINI_BOT_HOME": str(ROOT / "home"),
156
+ "GEMINI_BOT_SYSTEM_MD": str(workspace / ".gemini" / "system.md"),
157
+ "GEMINI_VOYAGER_MODEL": MODEL,
158
+ "GEMINI_VOYAGER_PROXY_PORT": str(PROXY_PORT),
159
+ "GEMINI_VOYAGER_EMBED_BASE": "http://127.0.0.1:7860/v1",
160
+ })
161
+ return env
162
+
163
+ def _start_proxy(self, env: dict[str, str]) -> None:
164
+ entrypoint = Path(__file__).resolve().parent / "gemini_voyager_proxy.py"
165
+ PROXY_LOG_PATH.write_text("", encoding="utf-8")
166
+ self.proxy_proc = subprocess.Popen(
167
+ [sys.executable, "-u", str(entrypoint)],
168
+ cwd=str(ROOT),
169
+ env=env,
170
+ stdout=subprocess.PIPE,
171
+ stderr=subprocess.STDOUT,
172
+ text=True,
173
+ bufsize=1,
174
+ )
175
+ threading.Thread(
176
+ target=self._pump,
177
+ args=(self.proxy_proc, PROXY_LOG_PATH, "gemini-voyager-proxy"),
178
+ daemon=True,
179
+ ).start()
180
+ if not _wait_tcp("127.0.0.1", PROXY_PORT, 60):
181
+ raise RuntimeError("Gemini Voyager proxy did not become ready")
182
+ if self.proxy_proc.poll() is not None:
183
+ raise RuntimeError(
184
+ f"Gemini Voyager proxy exited rc={self.proxy_proc.returncode}"
185
+ )
186
+
187
+ def _start_voyager(self, env: dict[str, str]) -> None:
188
+ runner = Path(__file__).resolve().parent / "voyager_runner.py"
189
+ voyager_env = env.copy()
190
+ voyager_env.update({
191
+ "VOYAGER_HOME": str(VOYAGER_SRC),
192
+ "VOYAGER_OPENAI_BASE": f"http://127.0.0.1:{PROXY_PORT}/v1",
193
+ "VOYAGER_API_KEY": "local-gemini-voyager",
194
+ "VOYAGER_MODEL": MODEL,
195
+ "VOYAGER_AUX_MODEL": MODEL,
196
+ "VOYAGER_CKPT_DIR": str(CKPT_DIR),
197
+ "MC_USERNAME": "Gemini_Bot",
198
+ "BOT_DISPLAY_NAME": "Gemini Bot",
199
+ "MC_PORT": "25565",
200
+ "MINEFLAYER_API_PORT": str(MINEFLAYER_PORT),
201
+ "MAX_ITERATIONS": os.environ.get(
202
+ "GEMINI_VOYAGER_MAX_ITERATIONS", "1000000"
203
+ ),
204
+ "OPENAI_API_REQUEST_TIMEOUT": "300",
205
+ "ANONYMIZED_TELEMETRY": "False",
206
+ "PYTHONPATH": str(VOYAGER_SRC),
207
+ })
208
+ LOG_PATH.write_text("", encoding="utf-8")
209
+ self.proc = subprocess.Popen(
210
+ [sys.executable, "-u", str(runner)],
211
+ cwd=str(VOYAGER_SRC),
212
+ env=voyager_env,
213
+ stdout=subprocess.PIPE,
214
+ stderr=subprocess.STDOUT,
215
+ text=True,
216
+ bufsize=1,
217
+ )
218
+ threading.Thread(
219
+ target=self._pump,
220
+ args=(self.proc, LOG_PATH, "gemini-voyager"),
221
+ daemon=True,
222
+ ).start()
223
 
224
  def _boot(self) -> None:
225
  self.started_at = time.time()
 
229
  self.error = ""
230
  if not _wait_tcp("127.0.0.1", 25565, 240):
231
  raise RuntimeError("Minecraft Java is unavailable")
232
+
233
+ ready_file = Path(os.environ.get(
234
+ "MINECRAFT_READY_FILE", "/tmp/minecraft-stack/minecraft.ready"
235
+ ))
236
  deadline = time.monotonic() + 240
237
+ while (
238
+ not self.stop_event.is_set()
239
+ and not ready_file.exists()
240
+ and time.monotonic() < deadline
241
+ ):
242
  self.stop_event.wait(1)
243
  if not ready_file.exists():
244
  raise RuntimeError("Minecraft world did not finish loading")
245
+
246
  if not any(os.environ.get(name) for name in KEY_NAMES):
247
  with self.lock:
248
  self.state = "waiting-credentials"
249
  self.error = "Set GEMINI_BOT_KEY_1 as a Space secret"
250
  return
251
+
252
  with self.lock:
253
  self.state = "preparing"
254
  cli, workspace = self._prepare()
255
+ env = self._child_env(cli, workspace)
 
 
 
 
 
 
 
 
 
 
 
256
  backoff = 5
257
+
258
  while not self.stop_event.is_set():
259
+ self._start_proxy(env)
260
+ self._start_voyager(env)
 
 
261
  time.sleep(3)
262
+ if self.proc.poll() is None and self.proxy_proc.poll() is None:
263
  with self.lock:
264
  self.state = "running"
265
  self.error = ""
266
+
267
+ while (
268
+ not self.stop_event.is_set()
269
+ and self.proc.poll() is None
270
+ and self.proxy_proc.poll() is None
271
+ ):
272
+ self.stop_event.wait(1)
273
+
274
  if self.stop_event.is_set():
275
  break
276
+
277
+ runner_rc = self.proc.returncode if self.proc else None
278
+ proxy_rc = self.proxy_proc.returncode if self.proxy_proc else None
279
+ _terminate(self.proc)
280
+ _terminate(self.proxy_proc)
281
  self.restart_count += 1
282
  with self.lock:
283
  self.state = "restarting"
284
+ self.error = (
285
+ f"Gemini Voyager child exit runner={runner_rc} "
286
+ f"proxy={proxy_rc}; retry {self.restart_count}"
287
+ )
288
  if self.stop_event.wait(backoff):
289
  break
290
  backoff = min(backoff * 2, 120)
291
+
292
+ _terminate(self.proc)
293
+ _terminate(self.proxy_proc)
294
  with self.lock:
295
  self.state = "stopped"
296
  except Exception as exc:
297
+ _terminate(self.proc)
298
+ _terminate(self.proxy_proc)
299
  with self.lock:
300
  self.state = "error"
301
+ self.error = str(exc)[:200]
302
+ print(
303
+ f"[gemini-voyager] startup error: {type(exc).__name__}: {self.error}",
304
+ flush=True,
305
+ )
306
 
307
  def start_async(self) -> dict[str, Any]:
308
  with self.lock:
309
  if self.worker and self.worker.is_alive():
310
  return self.status()
311
  self.stop_event.clear()
312
+ self.worker = threading.Thread(
313
+ target=self._boot, daemon=True, name="gemini-voyager-bootstrap"
314
+ )
315
  self.worker.start()
316
  return self.status()
317
 
318
  def stop(self) -> dict[str, Any]:
319
  self.stop_event.set()
320
  with self.lock:
321
+ runner = self.proc
322
+ proxy = self.proxy_proc
323
  self.state = "stopped"
324
+ _terminate(runner)
325
+ _terminate(proxy)
 
 
 
 
326
  return self.status()
327
 
328
+ def _mineflayer_status(self) -> dict[str, Any]:
329
  try:
330
+ with urllib.request.urlopen(
331
+ f"http://127.0.0.1:{MINEFLAYER_PORT}/health", timeout=1.5
332
+ ) as response:
333
+ data = json.load(response)
334
+ return data if isinstance(data, dict) else {"ready": False}
335
+ except Exception:
336
+ return {"ready": False}
337
+
338
+ def _proxy_status(self) -> dict[str, Any]:
339
+ try:
340
+ with urllib.request.urlopen(
341
+ f"http://127.0.0.1:{PROXY_PORT}/health", timeout=1.5
342
+ ) as response:
343
+ data = json.load(response)
344
+ return data if isinstance(data, dict) else {"ready": False}
345
+ except Exception:
346
+ return {"ready": False}
347
+
348
+ def status(self) -> dict[str, Any]:
349
+ completed = _read_json(CKPT_DIR / "curriculum" / "completed_tasks.json")
350
+ failed = _read_json(CKPT_DIR / "curriculum" / "failed_tasks.json")
351
+ mineflayer = self._mineflayer_status()
352
+ proxy = self._proxy_status()
353
  with self.lock:
354
  proc = self.proc
355
+ proxy_proc = self.proxy_proc
356
+ runner_running = bool(proc and proc.poll() is None)
357
+ proxy_running = bool(proxy_proc and proxy_proc.poll() is None)
358
  return {
359
  "state": self.state,
360
  "error": self.error or None,
361
+ "running": runner_running and proxy_running,
362
+ "pid": proc.pid if runner_running else None,
363
+ "proxy_pid": proxy_proc.pid if proxy_running else None,
364
  "restart_count": self.restart_count,
365
  "started_at": self.started_at,
366
+ "architecture": "Voyager",
367
+ "same_voyager_runtime_as_naldo": True,
368
  "bot_username": "Gemini_Bot",
369
  "model": MODEL,
370
  "cli_version": CLI_VERSION,
371
+ "configured_key_count": sum(
372
+ bool(os.environ.get(name)) for name in KEY_NAMES
373
+ ),
374
+ "checkpoint_dir": str(CKPT_DIR),
375
+ "checkpoint_persistent": str(CKPT_DIR).startswith("/persist/"),
376
+ "activity": {
377
+ "completed_count": len(completed),
378
+ "failed_count": len(failed),
379
+ "recent_completed": completed[-5:],
380
+ "recent_failed": failed[-8:],
381
+ },
382
+ "mineflayer": mineflayer,
383
+ "proxy": proxy,
384
+ "game": mineflayer,
385
  }
386
 
387
 
gemini_voyager_proxy.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenAI-compatible local adapter backed by Gemini CLI for Voyager."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import threading
8
+ import time
9
+ import urllib.request
10
+ import uuid
11
+ from typing import Any
12
+
13
+ from fastapi import FastAPI, HTTPException, Request
14
+ from fastapi.responses import JSONResponse, StreamingResponse
15
+
16
+ MODEL = os.environ.get("GEMINI_VOYAGER_MODEL", "gemini-3.5-flash-lite")
17
+ CLI = os.environ["GEMINI_BOT_CLI"]
18
+ WORKSPACE = os.environ["GEMINI_BOT_WORKSPACE"]
19
+ HOME = os.environ["GEMINI_BOT_HOME"]
20
+ SYSTEM_MD = os.environ["GEMINI_BOT_SYSTEM_MD"]
21
+ KEYS = [os.environ.get(f"GEMINI_BOT_KEY_{i}") for i in range(1, 7)]
22
+ KEYS = [key for key in KEYS if key]
23
+ EMBED_BASE = os.environ.get("GEMINI_VOYAGER_EMBED_BASE", "http://127.0.0.1:7860/v1")
24
+ LOCK = threading.Lock()
25
+ STATE_LOCK = threading.Lock()
26
+ state = {"calls": 0, "failures": 0, "selected_key": 0, "last_elapsed_s": None,
27
+ "prompt_tokens": 0, "completion_tokens": 0, "last_error": None}
28
+ api = FastAPI(title="Gemini Voyager Adapter", version="1.0.0")
29
+ def _content(value: Any) -> str:
30
+ if isinstance(value, str):
31
+ return value
32
+ if isinstance(value, list):
33
+ parts = []
34
+ for part in value:
35
+ if isinstance(part, dict) and isinstance(part.get("text"), str):
36
+ parts.append(part["text"])
37
+ return "\n".join(parts)
38
+ return str(value or "")
39
+
40
+
41
+ def _prompt(messages: list[dict[str, Any]]) -> str:
42
+ rendered = []
43
+ for message in messages:
44
+ role = str(message.get("role", "user")).upper()
45
+ rendered.append(f"[{role}]\n{_content(message.get('content'))}")
46
+ return (
47
+ "You are the language model inside the Voyager Minecraft agent. "
48
+ "Follow the supplied system and user instructions exactly. "
49
+ "Return only the response format requested by the final prompt; no wrapper commentary.\n\n"
50
+ + "\n\n".join(rendered)
51
+ )
52
+
53
+
54
+ def _usage(stats: dict[str, Any]) -> tuple[int, int]:
55
+ prompt = completion = 0
56
+ models = stats.get("models") if isinstance(stats, dict) else {}
57
+ if isinstance(models, dict):
58
+ for value in models.values():
59
+ tokens = value.get("tokens", {}) if isinstance(value, dict) else {}
60
+ prompt += int(tokens.get("prompt", tokens.get("input", tokens.get("inputTokens", 0))) or 0)
61
+ completion += int(tokens.get("candidates", tokens.get("output", tokens.get("outputTokens", 0))) or 0)
62
+ if prompt == 0 and completion == 0 and isinstance(stats, dict):
63
+ tokens = stats.get("tokens", {}) if isinstance(stats.get("tokens"), dict) else {}
64
+ prompt = int(tokens.get("prompt", tokens.get("input", tokens.get("inputTokens", 0))) or 0)
65
+ completion = int(tokens.get("candidates", tokens.get("output", tokens.get("outputTokens", 0))) or 0)
66
+ return prompt, completion
67
+ def _call_cli(prompt: str) -> tuple[str, int, int]:
68
+ if not KEYS:
69
+ raise RuntimeError("Gemini credentials unavailable")
70
+ with LOCK:
71
+ last = "Gemini CLI failed"
72
+ for _ in range(min(3, len(KEYS))):
73
+ with STATE_LOCK:
74
+ slot = state["selected_key"] % len(KEYS)
75
+ state["calls"] += 1
76
+ env = {
77
+ "PATH": os.environ.get("PATH", "/usr/bin:/bin"),
78
+ "HOME": HOME,
79
+ "GEMINI_CLI_HOME": HOME,
80
+ "GEMINI_CLI_TRUST_WORKSPACE": "true",
81
+ "GEMINI_SYSTEM_MD": SYSTEM_MD,
82
+ "GEMINI_TELEMETRY_ENABLED": "false",
83
+ "GEMINI_TELEMETRY_LOG_PROMPTS": "false",
84
+ "LANG": "C.UTF-8",
85
+ "GEMINI_API_KEY": KEYS[slot],
86
+ }
87
+ started = time.monotonic()
88
+ proc = subprocess.run(
89
+ [CLI, "--model", MODEL, "--output-format", "json", "--approval-mode", "plan",
90
+ "--prompt", prompt],
91
+ cwd=WORKSPACE, env=env, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
92
+ text=True, timeout=180, check=False,
93
+ )
94
+ elapsed = round(time.monotonic() - started, 1)
95
+ try:
96
+ envelope = json.loads(proc.stdout or "{}")
97
+ except ValueError:
98
+ envelope = {}
99
+ if proc.returncode == 0 and isinstance(envelope.get("response"), str):
100
+ p_tokens, c_tokens = _usage(envelope.get("stats") or {})
101
+ with STATE_LOCK:
102
+ state["last_elapsed_s"] = elapsed
103
+ state["prompt_tokens"] += p_tokens
104
+ state["completion_tokens"] += c_tokens
105
+ state["last_error"] = None
106
+ return envelope["response"], p_tokens, c_tokens
107
+ code = envelope.get("error", {}).get("code") if isinstance(envelope.get("error"), dict) else None
108
+ last = f"Gemini CLI exit {proc.returncode}" + (f" API {code}" if code is not None else "")
109
+ with STATE_LOCK:
110
+ state["selected_key"] = (slot + 1) % len(KEYS)
111
+ state["failures"] += 1
112
+ state["last_error"] = last
113
+ raise RuntimeError(last)
114
+
115
+
116
+ def _chat_payload(body: dict[str, Any]) -> dict[str, Any]:
117
+ answer, prompt_tokens, completion_tokens = _call_cli(_prompt(body.get("messages") or []))
118
+ return {
119
+ "id": "chatcmpl-" + uuid.uuid4().hex[:16],
120
+ "object": "chat.completion",
121
+ "created": int(time.time()),
122
+ "model": MODEL,
123
+ "choices": [{"index": 0, "message": {"role": "assistant", "content": answer},
124
+ "finish_reason": "stop"}],
125
+ "usage": {"prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens,
126
+ "total_tokens": prompt_tokens + completion_tokens},
127
+ }
128
+ @api.get("/health")
129
+ def health():
130
+ with STATE_LOCK:
131
+ snapshot = dict(state)
132
+ return {"ready": bool(KEYS), "model": MODEL, "configured_key_count": len(KEYS), **snapshot}
133
+
134
+
135
+ @api.get("/v1/models")
136
+ def models():
137
+ return {"object": "list", "data": [{"id": MODEL, "object": "model", "owned_by": "gemini-cli"}]}
138
+
139
+
140
+ @api.post("/v1/chat/completions")
141
+ async def chat(request: Request):
142
+ body = await request.json()
143
+ try:
144
+ payload = _chat_payload(body)
145
+ except Exception as exc:
146
+ raise HTTPException(status_code=503, detail=str(exc)[:160]) from None
147
+ if not body.get("stream"):
148
+ return JSONResponse(payload)
149
+ content = payload["choices"][0]["message"]["content"]
150
+ chunk = {"id": payload["id"], "object": "chat.completion.chunk", "created": payload["created"],
151
+ "model": MODEL, "choices": [{"index": 0, "delta": {"role": "assistant", "content": content},
152
+ "finish_reason": "stop"}]}
153
+ async def events():
154
+ yield "data: " + json.dumps(chunk, ensure_ascii=False) + "\n\n"
155
+ yield "data: [DONE]\n\n"
156
+ return StreamingResponse(events(), media_type="text/event-stream")
157
+ @api.post("/v1/embeddings")
158
+ async def embeddings(request: Request):
159
+ body = await request.body()
160
+ headers = {"Content-Type": "application/json"}
161
+ api_key = os.environ.get("API_KEY", "").strip()
162
+ if api_key:
163
+ headers["Authorization"] = f"Bearer {api_key}"
164
+ upstream = urllib.request.Request(
165
+ EMBED_BASE.rstrip("/") + "/embeddings", data=body,
166
+ headers=headers, method="POST",
167
+ )
168
+ try:
169
+ with urllib.request.urlopen(upstream, timeout=120) as response:
170
+ return JSONResponse(json.loads(response.read().decode("utf-8")))
171
+ except Exception as exc:
172
+ raise HTTPException(status_code=503, detail=f"embedding backend unavailable: {type(exc).__name__}") from None
173
+
174
+
175
+ if __name__ == "__main__":
176
+ import uvicorn
177
+ uvicorn.run(api, host="127.0.0.1", port=int(os.environ.get("GEMINI_VOYAGER_PROXY_PORT", "7863")),
178
+ log_level="warning")
tests/test_gemini_bot_stack.py CHANGED
@@ -1,4 +1,4 @@
1
- """Gemini_Bot stays separate from the persistent Voyager bot and credentials."""
2
  import ast
3
  import os
4
  from pathlib import Path
@@ -18,6 +18,9 @@ def test_isolation():
18
  with patch.dict(os.environ, {name: f"private-value-{i}" for i, name in enumerate(module.KEY_NAMES)}):
19
  status = module.GeminiBotStack().status()
20
  assert status["bot_username"] == "Gemini_Bot"
 
 
 
21
  assert status["configured_key_count"] == 6
22
  for name in module.KEY_NAMES:
23
  assert name not in str(status)
@@ -25,11 +28,15 @@ def test_isolation():
25
 
26
 
27
  def test_no_stored_credentials():
28
- for name in ["gemini_bot.js", "gemini_bot_stack.py", "gemini_bot_logic.js"]:
29
  source = (ROOT / name).read_text()
30
  assert "AIzaSy" not in source
31
  assert "AQ.Ab8" not in source
32
- tree = ast.parse((ROOT / "gemini_bot_stack.py").read_text())
 
 
 
 
33
  assert tree is not None
34
 
35
 
 
1
+ """Gemini_Bot runs the same Voyager architecture with isolated state and credentials."""
2
  import ast
3
  import os
4
  from pathlib import Path
 
18
  with patch.dict(os.environ, {name: f"private-value-{i}" for i, name in enumerate(module.KEY_NAMES)}):
19
  status = module.GeminiBotStack().status()
20
  assert status["bot_username"] == "Gemini_Bot"
21
+ assert status["architecture"] == "Voyager"
22
+ assert status["same_voyager_runtime_as_naldo"] is True
23
+ assert "gemini-voyager" in status["checkpoint_dir"].replace("\\", "/")
24
  assert status["configured_key_count"] == 6
25
  for name in module.KEY_NAMES:
26
  assert name not in str(status)
 
28
 
29
 
30
  def test_no_stored_credentials():
31
+ for name in ["gemini_bot.js", "gemini_bot_stack.py", "gemini_bot_logic.js", "gemini_voyager_proxy.py"]:
32
  source = (ROOT / name).read_text()
33
  assert "AIzaSy" not in source
34
  assert "AQ.Ab8" not in source
35
+ stack_source = (ROOT / "gemini_bot_stack.py").read_text()
36
+ assert "voyager_runner.py" in stack_source
37
+ assert "gemini_voyager_proxy.py" in stack_source
38
+ assert '"gemini_bot.js"' not in stack_source
39
+ tree = ast.parse(stack_source)
40
  assert tree is not None
41
 
42