naldo commited on
Commit ·
ef1d4f0
1
Parent(s): d17cab1
Bind Creative placement to task IDs and retain Gemini failure diagnostics
Browse files- README.md +23 -19
- gemini_voyager_proxy.py +9 -2
- tests/test_gemini_bot_stack.py +32 -0
- tests/test_voyager_patch.py +15 -4
- voyager_action_parse.py +19 -0
- voyager_runner.py +14 -0
- voyager_stack.py +20 -6
README.md
CHANGED
|
@@ -27,7 +27,7 @@ Endpoint OpenAI-compatible dedicado ao projeto original MineDojo/Voyager.
|
|
| 27 |
- Alias principal: `granite-4.0-h-tiny`
|
| 28 |
- CPU: 16 threads de decode e 16 threads de prefill/batch
|
| 29 |
|
| 30 |
-
Action, Curriculum, Critic e Skill Manager usam o
|
| 31 |
Curriculum em Creative usa contexto determinístico em vez de consultar QA
|
| 32 |
Survival para cada tarefa, reduzindo latência e evitando receitas/pré-requisitos
|
| 33 |
incompatíveis com o modo Creative.
|
|
@@ -52,9 +52,9 @@ O Voyager original usa embeddings no SkillManager e no cache de QA do Curriculum
|
|
| 52 |
Referência:
|
| 53 |
https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF
|
| 54 |
|
| 55 |
-
##
|
| 56 |
|
| 57 |
-
A inferência não
|
| 58 |
|
| 59 |
- `CUDA_VISIBLE_DEVICES=""`
|
| 60 |
- `HIP_VISIBLE_DEVICES=""`
|
|
@@ -73,16 +73,15 @@ a função não é chamada pela API.
|
|
| 73 |
|
| 74 |
## Compatibilidade Voyager
|
| 75 |
|
| 76 |
-
Por padrão `PLAYER_MODE=0`.
|
| 77 |
-
|
| 78 |
-
|
|
|
|
| 79 |
|
| 80 |
O endpoint aceita temperatura 0.0, que coincide com os defaults do Voyager para
|
| 81 |
ActionAgent, CurriculumAgent, CriticAgent e SkillManager.
|
| 82 |
|
| 83 |
-
|
| 84 |
-
Qwen3-Coder (`<tool_call>`, `<function=...>`, `<parameter=...>`) para `tool_calls` no
|
| 85 |
-
formato OpenAI. Isso não altera os prompts nem o loop do Voyager.
|
| 86 |
|
| 87 |
## API
|
| 88 |
|
|
@@ -92,22 +91,23 @@ formato OpenAI. Isso não altera os prompts nem o loop do Voyager.
|
|
| 92 |
- `GET /v1/models`
|
| 93 |
- `POST /v1/chat/completions`
|
| 94 |
- `POST /v1/embeddings`
|
|
|
|
|
|
|
| 95 |
- `GET /gemini-bot/status`
|
| 96 |
|
| 97 |
## Segundo jogador: Gemini_Bot
|
| 98 |
|
| 99 |
-
`Gemini_Bot`
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
e validadas: explorar, seguir, minerar um bloco próximo, colocar um bloco,
|
| 105 |
-
responder no chat ou aguardar. Mineração e colocação verificam o bloco real.
|
| 106 |
|
| 107 |
- CLI fixado em `@google/gemini-cli@0.61.0`, modelo efetivo `gemini-3.5-flash-lite`.
|
| 108 |
- Segredos privados do Space: `GEMINI_BOT_KEY_1` a `GEMINI_BOT_KEY_6`.
|
| 109 |
- Nenhuma chave deve ser salva no Git, no checkpoint ou nas saídas de status.
|
| 110 |
-
- Status: `/gemini-bot/status` (conexão,
|
|
|
|
| 111 |
- Controle de inicialização: `GEMINI_BOT_AUTOSTART=0` desativa somente o
|
| 112 |
segundo bot. O Naldo_Bot e o mundo persistente seguem independentes.
|
| 113 |
|
|
@@ -129,8 +129,12 @@ O comportamento do agente deve continuar vindo do projeto original:
|
|
| 129 |
https://github.com/MineDojo/Voyager
|
| 130 |
|
| 131 |
Este Space executa o Voyager fixado em `55e45a880755d0c8c66ca7fb5fe7962ac8974f89`
|
| 132 |
-
com correções de reconexão, observações nulas,
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
Validação offline, sem iniciar modelos ou servidor de jogo:
|
| 136 |
`python tests/test_voyager_patch.py /caminho/para/Voyager-oficial`
|
|
|
|
| 27 |
- Alias principal: `granite-4.0-h-tiny`
|
| 28 |
- CPU: 16 threads de decode e 16 threads de prefill/batch
|
| 29 |
|
| 30 |
+
No Naldo_Bot, Action, Curriculum, Critic e Skill Manager usam o Granite H-Tiny. O
|
| 31 |
Curriculum em Creative usa contexto determinístico em vez de consultar QA
|
| 32 |
Survival para cada tarefa, reduzindo latência e evitando receitas/pré-requisitos
|
| 33 |
incompatíveis com o modo Creative.
|
|
|
|
| 52 |
Referência:
|
| 53 |
https://huggingface.co/Qwen/Qwen3-Embedding-0.6B-GGUF
|
| 54 |
|
| 55 |
+
## Inferência local somente em CPU
|
| 56 |
|
| 57 |
+
A inferência Granite e os embeddings locais não usam GPU:
|
| 58 |
|
| 59 |
- `CUDA_VISIBLE_DEVICES=""`
|
| 60 |
- `HIP_VISIBLE_DEVICES=""`
|
|
|
|
| 73 |
|
| 74 |
## Compatibilidade Voyager
|
| 75 |
|
| 76 |
+
Por padrão `PLAYER_MODE=0`. O servidor não injeta o antigo prompt Bedrock.
|
| 77 |
+
Os dois Voyagers jogam em Creative: mineração, colocação, equipamento e inventário
|
| 78 |
+
são verificados a partir da observação real. O `placeBlocks` recebe o ID exato
|
| 79 |
+
da tarefa canônica; o agente não pode substituir `bricks` por `brick`.
|
| 80 |
|
| 81 |
O endpoint aceita temperatura 0.0, que coincide com os defaults do Voyager para
|
| 82 |
ActionAgent, CurriculumAgent, CriticAgent e SkillManager.
|
| 83 |
|
| 84 |
+
O endpoint mantém a compatibilidade HTTP de chat e embeddings para os clientes.
|
|
|
|
|
|
|
| 85 |
|
| 86 |
## API
|
| 87 |
|
|
|
|
| 91 |
- `GET /v1/models`
|
| 92 |
- `POST /v1/chat/completions`
|
| 93 |
- `POST /v1/embeddings`
|
| 94 |
+
- `GET /minecraft/status`
|
| 95 |
+
- `GET /voyager/status`
|
| 96 |
- `GET /gemini-bot/status`
|
| 97 |
|
| 98 |
## Segundo jogador: Gemini_Bot
|
| 99 |
|
| 100 |
+
`Gemini_Bot` executa uma segunda instância completa do Voyager no mesmo servidor
|
| 101 |
+
Java 1.19, com processo Mineflayer e checkpoint próprios. Curriculum, Action,
|
| 102 |
+
Critic e Skill Manager consultam o Gemini CLI por um proxy local exclusivo;
|
| 103 |
+
somente os embeddings usam o modelo local. `Naldo_Bot` continua usando Granite.
|
| 104 |
+
Ambos validam tarefas Creative pela mudança real no mundo ou pelo inventário.
|
|
|
|
|
|
|
| 105 |
|
| 106 |
- CLI fixado em `@google/gemini-cli@0.61.0`, modelo efetivo `gemini-3.5-flash-lite`.
|
| 107 |
- Segredos privados do Space: `GEMINI_BOT_KEY_1` a `GEMINI_BOT_KEY_6`.
|
| 108 |
- Nenhuma chave deve ser salva no Git, no checkpoint ou nas saídas de status.
|
| 109 |
+
- Status: `/gemini-bot/status` (conexão, tarefas, métricas e falhas por posição
|
| 110 |
+
da chave; nunca revela valores de chaves).
|
| 111 |
- Controle de inicialização: `GEMINI_BOT_AUTOSTART=0` desativa somente o
|
| 112 |
segundo bot. O Naldo_Bot e o mundo persistente seguem independentes.
|
| 113 |
|
|
|
|
| 129 |
https://github.com/MineDojo/Voyager
|
| 130 |
|
| 131 |
Este Space executa o Voyager fixado em `55e45a880755d0c8c66ca7fb5fe7962ac8974f89`
|
| 132 |
+
com correções de reconexão, observações nulas, exploração horizontal limitada e
|
| 133 |
+
mineração que não cava o chão sob o jogador. O mundo plano tem superfície em Y=64:
|
| 134 |
+
`/persist/minecraft-friends/world-20260927-flat-y64-v1`. Os checkpoints atuais
|
| 135 |
+
são `/persist/minecraft-friends/voyager/ckpt-20260927-flat-y64-v1` e
|
| 136 |
+
`/persist/minecraft-friends/gemini-voyager/ckpt-20260927-flat-y64-v1`.
|
| 137 |
+
A autenticação Xbox/Friends também persiste sob `/persist/minecraft-friends`.
|
| 138 |
|
| 139 |
Validação offline, sem iniciar modelos ou servidor de jogo:
|
| 140 |
`python tests/test_voyager_patch.py /caminho/para/Voyager-oficial`
|
gemini_voyager_proxy.py
CHANGED
|
@@ -27,7 +27,9 @@ EMBED_BASE = os.environ.get("GEMINI_VOYAGER_EMBED_BASE", "http://127.0.0.1:7860/
|
|
| 27 |
LOCK = threading.Lock()
|
| 28 |
STATE_LOCK = threading.Lock()
|
| 29 |
state = {"calls": 0, "failures": 0, "selected_key": 0, "last_elapsed_s": None,
|
| 30 |
-
"prompt_tokens": 0, "completion_tokens": 0, "last_error": None
|
|
|
|
|
|
|
| 31 |
api = FastAPI(title="Gemini Voyager Adapter", version="1.0.0")
|
| 32 |
def _content(value: Any) -> str:
|
| 33 |
if isinstance(value, str):
|
|
@@ -108,11 +110,16 @@ def _call_cli(prompt: str) -> tuple[str, int, int]:
|
|
| 108 |
state["last_error"] = None
|
| 109 |
return envelope["response"], p_tokens, c_tokens
|
| 110 |
code = envelope.get("error", {}).get("code") if isinstance(envelope.get("error"), dict) else None
|
| 111 |
-
|
|
|
|
| 112 |
with STATE_LOCK:
|
| 113 |
state["selected_key"] = (slot + 1) % len(KEYS)
|
| 114 |
state["failures"] += 1
|
| 115 |
state["last_error"] = last
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
raise RuntimeError(last)
|
| 117 |
|
| 118 |
|
|
|
|
| 27 |
LOCK = threading.Lock()
|
| 28 |
STATE_LOCK = threading.Lock()
|
| 29 |
state = {"calls": 0, "failures": 0, "selected_key": 0, "last_elapsed_s": None,
|
| 30 |
+
"prompt_tokens": 0, "completion_tokens": 0, "last_error": None,
|
| 31 |
+
"last_failure": None, "last_failure_at": None,
|
| 32 |
+
"last_failed_key_slot": None, "failures_by_key_slot": [0] * len(KEYS)}
|
| 33 |
api = FastAPI(title="Gemini Voyager Adapter", version="1.0.0")
|
| 34 |
def _content(value: Any) -> str:
|
| 35 |
if isinstance(value, str):
|
|
|
|
| 110 |
state["last_error"] = None
|
| 111 |
return envelope["response"], p_tokens, c_tokens
|
| 112 |
code = envelope.get("error", {}).get("code") if isinstance(envelope.get("error"), dict) else None
|
| 113 |
+
safe_code = str(code) if isinstance(code, (str, int)) and str(code).isalnum() and len(str(code)) <= 32 else None
|
| 114 |
+
last = f"Gemini CLI exit {proc.returncode}" + (f" API {safe_code}" if safe_code else "")
|
| 115 |
with STATE_LOCK:
|
| 116 |
state["selected_key"] = (slot + 1) % len(KEYS)
|
| 117 |
state["failures"] += 1
|
| 118 |
state["last_error"] = last
|
| 119 |
+
state["last_failure"] = last
|
| 120 |
+
state["last_failure_at"] = int(time.time())
|
| 121 |
+
state["last_failed_key_slot"] = slot + 1
|
| 122 |
+
state["failures_by_key_slot"][slot] += 1
|
| 123 |
raise RuntimeError(last)
|
| 124 |
|
| 125 |
|
tests/test_gemini_bot_stack.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
| 1 |
"""Gemini_Bot runs the same Voyager architecture with isolated state and credentials."""
|
| 2 |
import ast
|
|
|
|
| 3 |
import os
|
| 4 |
from pathlib import Path
|
|
|
|
| 5 |
import sys
|
| 6 |
from tempfile import TemporaryDirectory
|
|
|
|
|
|
|
|
|
|
| 7 |
from unittest.mock import patch
|
| 8 |
|
| 9 |
ROOT = Path(__file__).resolve().parents[1]
|
|
@@ -40,7 +45,34 @@ def test_no_stored_credentials():
|
|
| 40 |
assert tree is not None
|
| 41 |
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
if __name__ == "__main__":
|
| 44 |
test_isolation()
|
| 45 |
test_no_stored_credentials()
|
|
|
|
| 46 |
print("Gemini bot stack tests passed")
|
|
|
|
| 1 |
"""Gemini_Bot runs the same Voyager architecture with isolated state and credentials."""
|
| 2 |
import ast
|
| 3 |
+
import json
|
| 4 |
import os
|
| 5 |
from pathlib import Path
|
| 6 |
+
import subprocess
|
| 7 |
import sys
|
| 8 |
from tempfile import TemporaryDirectory
|
| 9 |
+
import threading
|
| 10 |
+
import time
|
| 11 |
+
from types import SimpleNamespace
|
| 12 |
from unittest.mock import patch
|
| 13 |
|
| 14 |
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
| 45 |
assert tree is not None
|
| 46 |
|
| 47 |
|
| 48 |
+
def test_proxy_preserves_safe_failure_diagnostics():
|
| 49 |
+
source = ast.parse((ROOT / "gemini_voyager_proxy.py").read_text())
|
| 50 |
+
func = next(node for node in source.body if isinstance(node, ast.FunctionDef) and node.name == "_call_cli")
|
| 51 |
+
state = {"calls": 0, "failures": 0, "selected_key": 0, "last_elapsed_s": None,
|
| 52 |
+
"prompt_tokens": 0, "completion_tokens": 0, "last_error": None,
|
| 53 |
+
"last_failure": None, "last_failure_at": None, "last_failed_key_slot": None,
|
| 54 |
+
"failures_by_key_slot": [0, 0]}
|
| 55 |
+
ns = {"KEYS": ["private-1", "private-2"], "LOCK": threading.Lock(),
|
| 56 |
+
"STATE_LOCK": threading.Lock(), "state": state, "os": os, "subprocess": subprocess,
|
| 57 |
+
"json": json, "time": time, "CLI": "gemini", "MODEL": "gemini-3.5-flash-lite",
|
| 58 |
+
"WORKSPACE": "/tmp", "HOME": "/tmp", "SYSTEM_MD": "/tmp/system.md",
|
| 59 |
+
"_usage": lambda stats: (12, 3)}
|
| 60 |
+
exec(compile(ast.Module(body=[func], type_ignores=[]), "proxy.py", "exec"), ns)
|
| 61 |
+
responses = [SimpleNamespace(returncode=1, stdout='{"error":{"code":429}}'),
|
| 62 |
+
SimpleNamespace(returncode=0, stdout='{"response":"ok","stats":{}}')]
|
| 63 |
+
with patch.object(subprocess, "run", side_effect=responses) as run:
|
| 64 |
+
assert ns["_call_cli"]("hello") == ("ok", 12, 3)
|
| 65 |
+
assert run.call_args_list[0].kwargs["env"]["GEMINI_API_KEY"] == "private-1"
|
| 66 |
+
assert run.call_args_list[1].kwargs["env"]["GEMINI_API_KEY"] == "private-2"
|
| 67 |
+
assert state["failures"] == 1 and state["last_error"] is None
|
| 68 |
+
assert state["last_failure"] == "Gemini CLI exit 1 API 429"
|
| 69 |
+
assert state["last_failed_key_slot"] == 1
|
| 70 |
+
assert state["failures_by_key_slot"] == [1, 0]
|
| 71 |
+
assert "private-" not in str(state)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
if __name__ == "__main__":
|
| 75 |
test_isolation()
|
| 76 |
test_no_stored_credentials()
|
| 77 |
+
test_proxy_preserves_safe_failure_diagnostics()
|
| 78 |
print("Gemini bot stack tests passed")
|
tests/test_voyager_patch.py
CHANGED
|
@@ -27,6 +27,7 @@ from voyager_verification import (
|
|
| 27 |
verify_task, quarantine_invalid_skills, task_history,
|
| 28 |
)
|
| 29 |
from voyager_action_parse import extract_action_code
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def functions(filename, names, namespace):
|
|
@@ -86,7 +87,7 @@ def test_resume():
|
|
| 86 |
file.parent.mkdir(parents=True, exist_ok=True)
|
| 87 |
file.write_text("{}")
|
| 88 |
original_completed = ["Mine 1 stone block to upgrade tools", "Mine 1 stone", "Place 4 snow_block"]
|
| 89 |
-
original_failed = ["Mine 1 stone", "Craft a stone pickaxe", "Craft a stone pickaxe"]
|
| 90 |
(ckpt / "curriculum/completed_tasks.json").write_text(json.dumps(original_completed))
|
| 91 |
(ckpt / "curriculum/failed_tasks.json").write_text(json.dumps(original_failed))
|
| 92 |
voyager = Mock()
|
|
@@ -132,6 +133,7 @@ def test_resume():
|
|
| 132 |
voyager.return_value.env.mineflayer.process.terminate.assert_called_once_with()
|
| 133 |
assert json.loads((ckpt / "curriculum/completed_tasks.json").read_text()) == original_completed[::2]
|
| 134 |
assert json.loads((ckpt / "curriculum/failed_tasks.json").read_text()) == ["Craft a stone pickaxe"]
|
|
|
|
| 135 |
archive = json.loads((ckpt / "quarantine/curriculum_history_before_semantic_compaction_v1.json").read_text())
|
| 136 |
assert archive == {"completed": original_completed, "failed": original_failed}
|
| 137 |
voyager.return_value.learn.side_effect = RuntimeError("fatal curriculum error")
|
|
@@ -239,15 +241,17 @@ def check_action_behavior(source):
|
|
| 239 |
def parse_js(code):
|
| 240 |
subprocess.run(["node", "--check", "-"], input=code, text=True,
|
| 241 |
capture_output=True, check=True)
|
| 242 |
-
|
|
|
|
| 243 |
params=[SimpleNamespace(type="Identifier", name="bot")])
|
| 244 |
node["async"] = True
|
|
|
|
| 245 |
return SimpleNamespace(program=SimpleNamespace(body=[node]))
|
| 246 |
|
| 247 |
-
generator = lambda node: SimpleNamespace(code="
|
| 248 |
require = lambda package: (SimpleNamespace(parseSync=parse_js) if package == "@babel/core"
|
| 249 |
else SimpleNamespace(default=generator))
|
| 250 |
-
ns = {"extract_action_code": extract_action_code, "require": require}
|
| 251 |
exec(compile(ast.Module(body=[method], type_ignores=[]), "action.py", "exec"), ns)
|
| 252 |
process = MethodType(ns["process_ai_message"], SimpleNamespace())
|
| 253 |
samples = [
|
|
@@ -271,6 +275,12 @@ def check_action_behavior(source):
|
|
| 271 |
assert "await placeStone(bot)" not in extract_action_code(content)
|
| 272 |
else:
|
| 273 |
assert "before program execution" in result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
print("PASS Action parser normalizes prose, rejects ambiguity, truncation and extra functions")
|
| 275 |
|
| 276 |
|
|
@@ -415,6 +425,7 @@ const bot = {entity:{position:{x:0.5,y:64,z:0.5}},
|
|
| 415 |
assert "Blocked invalid movement packet" in index
|
| 416 |
core = (stack.SRC / "voyager/voyager.py").read_text()
|
| 417 |
assert "Recovered Voyager observation without reconnecting Mineflayer" in core
|
|
|
|
| 418 |
assert "traceback.print_exc()" in core
|
| 419 |
|
| 420 |
|
|
|
|
| 27 |
verify_task, quarantine_invalid_skills, task_history,
|
| 28 |
)
|
| 29 |
from voyager_action_parse import extract_action_code
|
| 30 |
+
from voyager_action_parse import canonical_place_code
|
| 31 |
|
| 32 |
|
| 33 |
def functions(filename, names, namespace):
|
|
|
|
| 87 |
file.parent.mkdir(parents=True, exist_ok=True)
|
| 88 |
file.write_text("{}")
|
| 89 |
original_completed = ["Mine 1 stone block to upgrade tools", "Mine 1 stone", "Place 4 snow_block"]
|
| 90 |
+
original_failed = ["Mine 1 stone", "Craft a stone pickaxe", "Craft a stone pickaxe", "Place 1 bricks"]
|
| 91 |
(ckpt / "curriculum/completed_tasks.json").write_text(json.dumps(original_completed))
|
| 92 |
(ckpt / "curriculum/failed_tasks.json").write_text(json.dumps(original_failed))
|
| 93 |
voyager = Mock()
|
|
|
|
| 133 |
voyager.return_value.env.mineflayer.process.terminate.assert_called_once_with()
|
| 134 |
assert json.loads((ckpt / "curriculum/completed_tasks.json").read_text()) == original_completed[::2]
|
| 135 |
assert json.loads((ckpt / "curriculum/failed_tasks.json").read_text()) == ["Craft a stone pickaxe"]
|
| 136 |
+
assert json.loads((ckpt / "quarantine/bricks_block_id_failure_v1.json").read_text()) == {"failed": original_failed}
|
| 137 |
archive = json.loads((ckpt / "quarantine/curriculum_history_before_semantic_compaction_v1.json").read_text())
|
| 138 |
assert archive == {"completed": original_completed, "failed": original_failed}
|
| 139 |
voyager.return_value.learn.side_effect = RuntimeError("fatal curriculum error")
|
|
|
|
| 241 |
def parse_js(code):
|
| 242 |
subprocess.run(["node", "--check", "-"], input=code, text=True,
|
| 243 |
capture_output=True, check=True)
|
| 244 |
+
name = code.split("function ", 1)[1].split("(", 1)[0]
|
| 245 |
+
node = Node(type="FunctionDeclaration", id=SimpleNamespace(name=name),
|
| 246 |
params=[SimpleNamespace(type="Identifier", name="bot")])
|
| 247 |
node["async"] = True
|
| 248 |
+
node["source"] = code
|
| 249 |
return SimpleNamespace(program=SimpleNamespace(body=[node]))
|
| 250 |
|
| 251 |
+
generator = lambda node: SimpleNamespace(code=node["source"])
|
| 252 |
require = lambda package: (SimpleNamespace(parseSync=parse_js) if package == "@babel/core"
|
| 253 |
else SimpleNamespace(default=generator))
|
| 254 |
+
ns = {"extract_action_code": extract_action_code, "canonical_place_code": canonical_place_code, "require": require}
|
| 255 |
exec(compile(ast.Module(body=[method], type_ignores=[]), "action.py", "exec"), ns)
|
| 256 |
process = MethodType(ns["process_ai_message"], SimpleNamespace())
|
| 257 |
samples = [
|
|
|
|
| 275 |
assert "await placeStone(bot)" not in extract_action_code(content)
|
| 276 |
else:
|
| 277 |
assert "before program execution" in result
|
| 278 |
+
wrong_id = "```js\nasync function placeBrick(bot) { await placeBlocks(bot, 'brick_block', 4); }\n```"
|
| 279 |
+
repaired = process(SimpleNamespace(content=wrong_id), task="Place 1 bricks")
|
| 280 |
+
assert repaired["program_name"] == "placeBrick"
|
| 281 |
+
assert 'placeBlocks(bot, "bricks", 1)' in repaired["program_code"]
|
| 282 |
+
assert "brick_block" not in repaired["program_code"]
|
| 283 |
+
assert "brick_block" in process(SimpleNamespace(content=wrong_id), task="Mine 1 stone")["program_code"]
|
| 284 |
print("PASS Action parser normalizes prose, rejects ambiguity, truncation and extra functions")
|
| 285 |
|
| 286 |
|
|
|
|
| 425 |
assert "Blocked invalid movement packet" in index
|
| 426 |
core = (stack.SRC / "voyager/voyager.py").read_text()
|
| 427 |
assert "Recovered Voyager observation without reconnecting Mineflayer" in core
|
| 428 |
+
assert "process_ai_message(message=ai_message, task=self.task)" in core
|
| 429 |
assert "traceback.print_exc()" in core
|
| 430 |
|
| 431 |
|
voyager_action_parse.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
| 1 |
"""Extract one executable Action function from small-model responses."""
|
| 2 |
|
|
|
|
| 3 |
import re
|
| 4 |
|
|
|
|
|
|
|
| 5 |
|
| 6 |
_FENCE = re.compile(r"```([^\n`]*)\n(.*?)```", re.DOTALL)
|
| 7 |
_FUNCTION = re.compile(
|
|
@@ -79,3 +82,19 @@ def extract_action_code(response: str) -> str:
|
|
| 79 |
if len(functions) != 1:
|
| 80 |
raise ValueError(f"Expected exactly one complete Action function; found {len(functions)}")
|
| 81 |
return functions[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""Extract one executable Action function from small-model responses."""
|
| 2 |
|
| 3 |
+
import json
|
| 4 |
import re
|
| 5 |
|
| 6 |
+
from voyager_verification import SAFE_PLACE_BLOCKS, canonical_task, task_spec
|
| 7 |
+
|
| 8 |
|
| 9 |
_FENCE = re.compile(r"```([^\n`]*)\n(.*?)```", re.DOTALL)
|
| 10 |
_FUNCTION = re.compile(
|
|
|
|
| 82 |
if len(functions) != 1:
|
| 83 |
raise ValueError(f"Expected exactly one complete Action function; found {len(functions)}")
|
| 84 |
return functions[0]
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def canonical_place_code(task: str, function_name: str) -> str | None:
|
| 88 |
+
"""Bind a Creative placement to the verified task, not a guessed block ID."""
|
| 89 |
+
if canonical_task(task) != task:
|
| 90 |
+
return None
|
| 91 |
+
spec = task_spec(task)
|
| 92 |
+
if not spec or spec[0] != "place" or spec[2] not in SAFE_PLACE_BLOCKS:
|
| 93 |
+
return None
|
| 94 |
+
if not re.fullmatch(r"[A-Za-z_$][\w$]*", function_name):
|
| 95 |
+
raise ValueError("Invalid Action function name")
|
| 96 |
+
_, count, name = spec
|
| 97 |
+
return (
|
| 98 |
+
f"async function {function_name}(bot) {{ "
|
| 99 |
+
f"await placeBlocks(bot, {json.dumps(name)}, {count}); }}"
|
| 100 |
+
)
|
voyager_runner.py
CHANGED
|
@@ -107,6 +107,20 @@ def main() -> None:
|
|
| 107 |
repair_marker.write_text("ok\n", encoding="utf-8")
|
| 108 |
print("[voyager] removed false starter-task failure from missing Creative context", flush=True)
|
| 109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
for path, values, original in ((completed_path, completed, original_completed), (failed_path, failed, original_failed)):
|
| 111 |
if values != original:
|
| 112 |
temporary = path.with_suffix(".json.tmp")
|
|
|
|
| 107 |
repair_marker.write_text("ok\n", encoding="utf-8")
|
| 108 |
print("[voyager] removed false starter-task failure from missing Creative context", flush=True)
|
| 109 |
|
| 110 |
+
# Granite called placeBlocks("brick") / ("brick_block") for the
|
| 111 |
+
# legitimate "Place 1 bricks" task. Both failed before changing
|
| 112 |
+
# the world. Archive the failure before allowing one fresh attempt
|
| 113 |
+
# with the exact task ID bound by the Action parser.
|
| 114 |
+
bricks_marker = ckpt / ".bricks_block_id_repair_v1"
|
| 115 |
+
if bot_username == "Naldo_Bot" and not bricks_marker.exists():
|
| 116 |
+
if "Place 1 bricks" in failed and "Place 1 bricks" not in completed:
|
| 117 |
+
bricks_archive = ckpt / "quarantine" / "bricks_block_id_failure_v1.json"
|
| 118 |
+
bricks_archive.parent.mkdir(parents=True, exist_ok=True)
|
| 119 |
+
bricks_archive.write_text(json.dumps({"failed": original_failed}, indent=2) + "\n", encoding="utf-8")
|
| 120 |
+
failed = [item for item in failed if item != "Place 1 bricks"]
|
| 121 |
+
print("[voyager] archived invalid brick-ID failure for one retry", flush=True)
|
| 122 |
+
bricks_marker.write_text("ok\n", encoding="utf-8")
|
| 123 |
+
|
| 124 |
for path, values, original in ((completed_path, completed, original_completed), (failed_path, failed, original_failed)):
|
| 125 |
if values != original:
|
| 126 |
temporary = path.with_suffix(".json.tmp")
|
voyager_stack.py
CHANGED
|
@@ -520,7 +520,7 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
|
|
| 520 |
' observation += f"Critique: {critique}\\n\\n"',
|
| 521 |
' observation += f"Critique: {critique[:1200]}\\n\\n"',
|
| 522 |
)
|
| 523 |
-
safe_action_parser = ''' def process_ai_message(self, message):
|
| 524 |
# Do not concatenate fences or try the same malformed response 3 times.
|
| 525 |
# The caller retries with fresh feedback while Mineflayer stays connected.
|
| 526 |
try:
|
|
@@ -535,6 +535,12 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
|
|
| 535 |
params = list(node["params"])
|
| 536 |
if len(params) != 1 or params[0].type != "Identifier" or params[0].name != "bot":
|
| 537 |
raise ValueError("Action function must take exactly one bot argument")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
return {
|
| 539 |
"program_code": babel_generator(node).code,
|
| 540 |
"program_name": node.id.name,
|
|
@@ -552,7 +558,7 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
|
|
| 552 |
)
|
| 553 |
if action_parser_count != 1:
|
| 554 |
raise RuntimeError("Voyager Action parser marker changed")
|
| 555 |
-
action_agent_text = 'from voyager_action_parse import extract_action_code\n' + action_agent_text
|
| 556 |
action_agent_path.write_text(action_agent_text, encoding="utf-8")
|
| 557 |
|
| 558 |
# Bound curriculum memory rendered into every task proposal. The full
|
|
@@ -901,16 +907,24 @@ async function exploreUntil(bot, direction, maxTime = 60, callback) {
|
|
| 901 |
# be parsed. EventRecorder immediately indexes events[0], so a truncated
|
| 902 |
# LLM response turns a recoverable parse retry into IndexError. Record
|
| 903 |
# the last valid observation instead and let Voyager retry the action.
|
| 904 |
-
voyager_src = voyager_src.replace(
|
| 905 |
-
''' self.recorder.record([], self.task)
|
| 906 |
print(f"\\033[34m{parsed_result} Trying again!\\033[0m")
|
| 907 |
''',
|
| 908 |
''' if self.last_events:
|
| 909 |
self.recorder.record(self.last_events, self.task)
|
| 910 |
print(f"\\033[34m{parsed_result} Trying again!\\033[0m")
|
| 911 |
''',
|
| 912 |
-
1,
|
| 913 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
|
| 915 |
old_recovery = ''' except Exception as e:\n time.sleep(3) # wait for mineflayer to exit\n info = {\n "task": task,\n "success": False,\n }\n # reset bot status here\n self.last_events = self.env.reset(\n options={\n "mode": "hard",\n "wait_ticks": self.env_wait_ticks,\n "inventory": self.last_events[-1][1]["inventory"],\n "equipment": self.last_events[-1][1]["status"]["equipment"],\n "position": self.last_events[-1][1]["status"]["position"],\n }\n )\n # use red color background to print the error\n print("Your last round rollout terminated due to error:")\n print(f"\\033[41m{e}\\033[0m")\n'''
|
| 916 |
new_recovery = ''' except Exception as e:\n info = {\n "task": task,\n "success": False,\n }\n print("Your last round rollout terminated due to error:")\n traceback.print_exc()\n print(f"\\033[41m{type(e).__name__}: {e}\\033[0m")\n try:\n # Preserve the current Mineflayer connection whenever it is\n # still responsive. This prevents needless leave/rejoin.\n self.last_events = self.env.step("")\n print("Recovered Voyager observation without reconnecting Mineflayer")\n except Exception as recovery_error:\n print("Live recovery failed; performing one hard Mineflayer reset")\n traceback.print_exc()\n previous = self.last_events[-1][1] if self.last_events else {}\n status = previous.get("status", {}) if isinstance(previous, dict) else {}\n self.last_events = self.env.reset(\n options={\n "mode": "hard",\n "wait_ticks": self.env_wait_ticks,\n "inventory": previous.get("inventory", {}) if isinstance(previous, dict) else {},\n "equipment": status.get("equipment", []),\n "position": status.get("position", None),\n }\n )\n'''
|
|
|
|
| 520 |
' observation += f"Critique: {critique}\\n\\n"',
|
| 521 |
' observation += f"Critique: {critique[:1200]}\\n\\n"',
|
| 522 |
)
|
| 523 |
+
safe_action_parser = ''' def process_ai_message(self, message, task=None):
|
| 524 |
# Do not concatenate fences or try the same malformed response 3 times.
|
| 525 |
# The caller retries with fresh feedback while Mineflayer stays connected.
|
| 526 |
try:
|
|
|
|
| 535 |
params = list(node["params"])
|
| 536 |
if len(params) != 1 or params[0].type != "Identifier" or params[0].name != "bot":
|
| 537 |
raise ValueError("Action function must take exactly one bot argument")
|
| 538 |
+
# A valid Place task supplies its exact Minecraft block ID. Small
|
| 539 |
+
# models sometimes change "bricks" into the non-block "brick";
|
| 540 |
+
# retain the function name while binding the primitive to the task.
|
| 541 |
+
place_code = canonical_place_code(task, node.id.name) if task else None
|
| 542 |
+
if place_code:
|
| 543 |
+
node = babel.parseSync(place_code).program.body[0]
|
| 544 |
return {
|
| 545 |
"program_code": babel_generator(node).code,
|
| 546 |
"program_name": node.id.name,
|
|
|
|
| 558 |
)
|
| 559 |
if action_parser_count != 1:
|
| 560 |
raise RuntimeError("Voyager Action parser marker changed")
|
| 561 |
+
action_agent_text = 'from voyager_action_parse import extract_action_code, canonical_place_code\n' + action_agent_text
|
| 562 |
action_agent_path.write_text(action_agent_text, encoding="utf-8")
|
| 563 |
|
| 564 |
# Bound curriculum memory rendered into every task proposal. The full
|
|
|
|
| 907 |
# be parsed. EventRecorder immediately indexes events[0], so a truncated
|
| 908 |
# LLM response turns a recoverable parse retry into IndexError. Record
|
| 909 |
# the last valid observation instead and let Voyager retry the action.
|
| 910 |
+
voyager_src = voyager_src.replace(
|
| 911 |
+
''' self.recorder.record([], self.task)
|
| 912 |
print(f"\\033[34m{parsed_result} Trying again!\\033[0m")
|
| 913 |
''',
|
| 914 |
''' if self.last_events:
|
| 915 |
self.recorder.record(self.last_events, self.task)
|
| 916 |
print(f"\\033[34m{parsed_result} Trying again!\\033[0m")
|
| 917 |
''',
|
| 918 |
+
1,
|
| 919 |
+
)
|
| 920 |
+
action_call = ' parsed_result = self.action_agent.process_ai_message(message=ai_message)'
|
| 921 |
+
if voyager_src.count(action_call) != 1:
|
| 922 |
+
raise RuntimeError("Voyager Action call marker changed")
|
| 923 |
+
voyager_src = voyager_src.replace(
|
| 924 |
+
action_call,
|
| 925 |
+
' parsed_result = self.action_agent.process_ai_message(message=ai_message, task=self.task)',
|
| 926 |
+
1,
|
| 927 |
+
)
|
| 928 |
|
| 929 |
old_recovery = ''' except Exception as e:\n time.sleep(3) # wait for mineflayer to exit\n info = {\n "task": task,\n "success": False,\n }\n # reset bot status here\n self.last_events = self.env.reset(\n options={\n "mode": "hard",\n "wait_ticks": self.env_wait_ticks,\n "inventory": self.last_events[-1][1]["inventory"],\n "equipment": self.last_events[-1][1]["status"]["equipment"],\n "position": self.last_events[-1][1]["status"]["position"],\n }\n )\n # use red color background to print the error\n print("Your last round rollout terminated due to error:")\n print(f"\\033[41m{e}\\033[0m")\n'''
|
| 930 |
new_recovery = ''' except Exception as e:\n info = {\n "task": task,\n "success": False,\n }\n print("Your last round rollout terminated due to error:")\n traceback.print_exc()\n print(f"\\033[41m{type(e).__name__}: {e}\\033[0m")\n try:\n # Preserve the current Mineflayer connection whenever it is\n # still responsive. This prevents needless leave/rejoin.\n self.last_events = self.env.step("")\n print("Recovered Voyager observation without reconnecting Mineflayer")\n except Exception as recovery_error:\n print("Live recovery failed; performing one hard Mineflayer reset")\n traceback.print_exc()\n previous = self.last_events[-1][1] if self.last_events else {}\n status = previous.get("status", {}) if isinstance(previous, dict) else {}\n self.last_events = self.env.reset(\n options={\n "mode": "hard",\n "wait_ticks": self.env_wait_ticks,\n "inventory": previous.get("inventory", {}) if isinstance(previous, dict) else {},\n "equipment": status.get("equipment", []),\n "position": status.get("position", None),\n }\n )\n'''
|