gymcraft 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. gymcraft-1.2.0/PKG-INFO +167 -0
  2. gymcraft-1.2.0/README.md +154 -0
  3. gymcraft-1.2.0/pyproject.toml +67 -0
  4. gymcraft-1.2.0/setup.cfg +4 -0
  5. gymcraft-1.2.0/src/gymcraft/__init__.py +12 -0
  6. gymcraft-1.2.0/src/gymcraft/client.py +159 -0
  7. gymcraft-1.2.0/src/gymcraft/gym/action/action_pb2.py +38 -0
  8. gymcraft-1.2.0/src/gymcraft/gym/action/action_pb2.pyi +15 -0
  9. gymcraft-1.2.0/src/gymcraft/gym/action/action_pb2_grpc.py +24 -0
  10. gymcraft-1.2.0/src/gymcraft/gym/action/components/attack_once_pb2.py +37 -0
  11. gymcraft-1.2.0/src/gymcraft/gym/action/components/attack_once_pb2.pyi +11 -0
  12. gymcraft-1.2.0/src/gymcraft/gym/action/components/attack_once_pb2_grpc.py +24 -0
  13. gymcraft-1.2.0/src/gymcraft/gym/action/components/break_block_pb2.py +37 -0
  14. gymcraft-1.2.0/src/gymcraft/gym/action/components/break_block_pb2.pyi +15 -0
  15. gymcraft-1.2.0/src/gymcraft/gym/action/components/break_block_pb2_grpc.py +24 -0
  16. gymcraft-1.2.0/src/gymcraft/gym/action/components/click_menu_button_pb2.py +37 -0
  17. gymcraft-1.2.0/src/gymcraft/gym/action/components/click_menu_button_pb2.pyi +13 -0
  18. gymcraft-1.2.0/src/gymcraft/gym/action/components/click_menu_button_pb2_grpc.py +24 -0
  19. gymcraft-1.2.0/src/gymcraft/gym/action/components/close_menu_pb2.py +37 -0
  20. gymcraft-1.2.0/src/gymcraft/gym/action/components/close_menu_pb2.pyi +11 -0
  21. gymcraft-1.2.0/src/gymcraft/gym/action/components/close_menu_pb2_grpc.py +24 -0
  22. gymcraft-1.2.0/src/gymcraft/gym/action/components/drop_item_pb2.py +37 -0
  23. gymcraft-1.2.0/src/gymcraft/gym/action/components/drop_item_pb2.pyi +13 -0
  24. gymcraft-1.2.0/src/gymcraft/gym/action/components/drop_item_pb2_grpc.py +24 -0
  25. gymcraft-1.2.0/src/gymcraft/gym/action/components/jump_pb2.py +37 -0
  26. gymcraft-1.2.0/src/gymcraft/gym/action/components/jump_pb2.pyi +9 -0
  27. gymcraft-1.2.0/src/gymcraft/gym/action/components/jump_pb2_grpc.py +24 -0
  28. gymcraft-1.2.0/src/gymcraft/gym/action/components/look_at_pb2.py +43 -0
  29. gymcraft-1.2.0/src/gymcraft/gym/action/components/look_at_pb2.pyi +38 -0
  30. gymcraft-1.2.0/src/gymcraft/gym/action/components/look_at_pb2_grpc.py +24 -0
  31. gymcraft-1.2.0/src/gymcraft/gym/action/components/move_menu_item_pb2.py +39 -0
  32. gymcraft-1.2.0/src/gymcraft/gym/action/components/move_menu_item_pb2.pyi +27 -0
  33. gymcraft-1.2.0/src/gymcraft/gym/action/components/move_menu_item_pb2_grpc.py +24 -0
  34. gymcraft-1.2.0/src/gymcraft/gym/action/components/move_to_pb2.py +37 -0
  35. gymcraft-1.2.0/src/gymcraft/gym/action/components/move_to_pb2.pyi +17 -0
  36. gymcraft-1.2.0/src/gymcraft/gym/action/components/move_to_pb2_grpc.py +24 -0
  37. gymcraft-1.2.0/src/gymcraft/gym/action/components/noop_pb2.py +37 -0
  38. gymcraft-1.2.0/src/gymcraft/gym/action/components/noop_pb2.pyi +9 -0
  39. gymcraft-1.2.0/src/gymcraft/gym/action/components/noop_pb2_grpc.py +24 -0
  40. gymcraft-1.2.0/src/gymcraft/gym/action/components/open_menu_pb2.py +43 -0
  41. gymcraft-1.2.0/src/gymcraft/gym/action/components/open_menu_pb2.pyi +36 -0
  42. gymcraft-1.2.0/src/gymcraft/gym/action/components/open_menu_pb2_grpc.py +24 -0
  43. gymcraft-1.2.0/src/gymcraft/gym/action/components/pick_up_item_pb2.py +37 -0
  44. gymcraft-1.2.0/src/gymcraft/gym/action/components/pick_up_item_pb2.pyi +11 -0
  45. gymcraft-1.2.0/src/gymcraft/gym/action/components/pick_up_item_pb2_grpc.py +24 -0
  46. gymcraft-1.2.0/src/gymcraft/gym/action/components/send_chat_pb2.py +37 -0
  47. gymcraft-1.2.0/src/gymcraft/gym/action/components/send_chat_pb2.pyi +11 -0
  48. gymcraft-1.2.0/src/gymcraft/gym/action/components/send_chat_pb2_grpc.py +24 -0
  49. gymcraft-1.2.0/src/gymcraft/gym/action/components/set_attack_target_pb2.py +37 -0
  50. gymcraft-1.2.0/src/gymcraft/gym/action/components/set_attack_target_pb2.pyi +13 -0
  51. gymcraft-1.2.0/src/gymcraft/gym/action/components/set_attack_target_pb2_grpc.py +24 -0
  52. gymcraft-1.2.0/src/gymcraft/gym/action/components/set_block_pb2.py +37 -0
  53. gymcraft-1.2.0/src/gymcraft/gym/action/components/set_block_pb2.pyi +17 -0
  54. gymcraft-1.2.0/src/gymcraft/gym/action/components/set_block_pb2_grpc.py +24 -0
  55. gymcraft-1.2.0/src/gymcraft/gym/action/components/step_move_pb2.py +37 -0
  56. gymcraft-1.2.0/src/gymcraft/gym/action/components/step_move_pb2.pyi +17 -0
  57. gymcraft-1.2.0/src/gymcraft/gym/action/components/step_move_pb2_grpc.py +24 -0
  58. gymcraft-1.2.0/src/gymcraft/gym/action/components/update_interesting_blocks_pb2.py +37 -0
  59. gymcraft-1.2.0/src/gymcraft/gym/action/components/update_interesting_blocks_pb2.pyi +15 -0
  60. gymcraft-1.2.0/src/gymcraft/gym/action/components/update_interesting_blocks_pb2_grpc.py +24 -0
  61. gymcraft-1.2.0/src/gymcraft/gym/action/components/use_item_pb2.py +41 -0
  62. gymcraft-1.2.0/src/gymcraft/gym/action/components/use_item_pb2.pyi +32 -0
  63. gymcraft-1.2.0/src/gymcraft/gym/action/components/use_item_pb2_grpc.py +24 -0
  64. gymcraft-1.2.0/src/gymcraft/gym/observation/common/block_view_pb2.py +37 -0
  65. gymcraft-1.2.0/src/gymcraft/gym/observation/common/block_view_pb2.pyi +19 -0
  66. gymcraft-1.2.0/src/gymcraft/gym/observation/common/block_view_pb2_grpc.py +24 -0
  67. gymcraft-1.2.0/src/gymcraft/gym/observation/common/entity_view_pb2.py +37 -0
  68. gymcraft-1.2.0/src/gymcraft/gym/observation/common/entity_view_pb2.pyi +37 -0
  69. gymcraft-1.2.0/src/gymcraft/gym/observation/common/entity_view_pb2_grpc.py +24 -0
  70. gymcraft-1.2.0/src/gymcraft/gym/observation/common/item_entity_view_pb2.py +38 -0
  71. gymcraft-1.2.0/src/gymcraft/gym/observation/common/item_entity_view_pb2.pyi +25 -0
  72. gymcraft-1.2.0/src/gymcraft/gym/observation/common/item_entity_view_pb2_grpc.py +24 -0
  73. gymcraft-1.2.0/src/gymcraft/gym/observation/common/item_stack_view_pb2.py +37 -0
  74. gymcraft-1.2.0/src/gymcraft/gym/observation/common/item_stack_view_pb2.pyi +15 -0
  75. gymcraft-1.2.0/src/gymcraft/gym/observation/common/item_stack_view_pb2_grpc.py +24 -0
  76. gymcraft-1.2.0/src/gymcraft/gym/observation/common/slot_pb2.py +38 -0
  77. gymcraft-1.2.0/src/gymcraft/gym/observation/common/slot_pb2.pyi +21 -0
  78. gymcraft-1.2.0/src/gymcraft/gym/observation/common/slot_pb2_grpc.py +24 -0
  79. gymcraft-1.2.0/src/gymcraft/gym/observation/components/chat_pb2.py +39 -0
  80. gymcraft-1.2.0/src/gymcraft/gym/observation/components/chat_pb2.pyi +23 -0
  81. gymcraft-1.2.0/src/gymcraft/gym/observation/components/chat_pb2_grpc.py +24 -0
  82. gymcraft-1.2.0/src/gymcraft/gym/observation/components/interesting_blocks_pb2.py +38 -0
  83. gymcraft-1.2.0/src/gymcraft/gym/observation/components/interesting_blocks_pb2.pyi +14 -0
  84. gymcraft-1.2.0/src/gymcraft/gym/observation/components/interesting_blocks_pb2_grpc.py +24 -0
  85. gymcraft-1.2.0/src/gymcraft/gym/observation/components/menu_pb2.py +42 -0
  86. gymcraft-1.2.0/src/gymcraft/gym/observation/components/menu_pb2.pyi +46 -0
  87. gymcraft-1.2.0/src/gymcraft/gym/observation/components/menu_pb2_grpc.py +24 -0
  88. gymcraft-1.2.0/src/gymcraft/gym/observation/components/nearby_blocks_pb2.py +38 -0
  89. gymcraft-1.2.0/src/gymcraft/gym/observation/components/nearby_blocks_pb2.pyi +14 -0
  90. gymcraft-1.2.0/src/gymcraft/gym/observation/components/nearby_blocks_pb2_grpc.py +24 -0
  91. gymcraft-1.2.0/src/gymcraft/gym/observation/components/nearby_entities_pb2.py +38 -0
  92. gymcraft-1.2.0/src/gymcraft/gym/observation/components/nearby_entities_pb2.pyi +14 -0
  93. gymcraft-1.2.0/src/gymcraft/gym/observation/components/nearby_entities_pb2_grpc.py +24 -0
  94. gymcraft-1.2.0/src/gymcraft/gym/observation/components/nearby_items_pb2.py +38 -0
  95. gymcraft-1.2.0/src/gymcraft/gym/observation/components/nearby_items_pb2.pyi +14 -0
  96. gymcraft-1.2.0/src/gymcraft/gym/observation/components/nearby_items_pb2_grpc.py +24 -0
  97. gymcraft-1.2.0/src/gymcraft/gym/observation/components/self_pb2.py +37 -0
  98. gymcraft-1.2.0/src/gymcraft/gym/observation/components/self_pb2.pyi +49 -0
  99. gymcraft-1.2.0/src/gymcraft/gym/observation/components/self_pb2_grpc.py +24 -0
  100. gymcraft-1.2.0/src/gymcraft/gym/observation/components/world_pb2.py +39 -0
  101. gymcraft-1.2.0/src/gymcraft/gym/observation/components/world_pb2.pyi +39 -0
  102. gymcraft-1.2.0/src/gymcraft/gym/observation/components/world_pb2_grpc.py +24 -0
  103. gymcraft-1.2.0/src/gymcraft/gym/observation/observation_pb2.py +44 -0
  104. gymcraft-1.2.0/src/gymcraft/gym/observation/observation_pb2.pyi +37 -0
  105. gymcraft-1.2.0/src/gymcraft/gym/observation/observation_pb2_grpc.py +24 -0
  106. gymcraft-1.2.0/src/gymcraft/gym/rpc/env_service_pb2.py +55 -0
  107. gymcraft-1.2.0/src/gymcraft/gym/rpc/env_service_pb2.pyi +83 -0
  108. gymcraft-1.2.0/src/gymcraft/gym/rpc/env_service_pb2_grpc.py +226 -0
  109. gymcraft-1.2.0/src/gymcraft/llm/__init__.py +27 -0
  110. gymcraft-1.2.0/src/gymcraft/llm/actions.py +647 -0
  111. gymcraft-1.2.0/src/gymcraft/llm/context.py +92 -0
  112. gymcraft-1.2.0/src/gymcraft/llm/env.py +187 -0
  113. gymcraft-1.2.0/src/gymcraft/llm/observations.py +351 -0
  114. gymcraft-1.2.0/src/gymcraft/llm/types.py +68 -0
  115. gymcraft-1.2.0/src/gymcraft/type_info.py +109 -0
  116. gymcraft-1.2.0/src/gymcraft.egg-info/PKG-INFO +167 -0
  117. gymcraft-1.2.0/src/gymcraft.egg-info/SOURCES.txt +121 -0
  118. gymcraft-1.2.0/src/gymcraft.egg-info/dependency_links.txt +1 -0
  119. gymcraft-1.2.0/src/gymcraft.egg-info/requires.txt +6 -0
  120. gymcraft-1.2.0/src/gymcraft.egg-info/top_level.txt +1 -0
  121. gymcraft-1.2.0/tests/test_interesting_blocks.py +49 -0
  122. gymcraft-1.2.0/tests/test_llm_tools.py +566 -0
  123. gymcraft-1.2.0/tests/test_use_item.py +56 -0
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: gymcraft
3
+ Version: 1.2.0
4
+ Summary: Python RPC client for GymCraft - Gymnasium-style RL environments in Minecraft
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: grpcio>=1.81.1
8
+ Requires-Dist: gymnasium>=1.3.0
9
+ Requires-Dist: mcrcon>=0.7.0
10
+ Requires-Dist: mypy>=2.1.0
11
+ Requires-Dist: openai>=3.3.1
12
+ Requires-Dist: protobuf>=6.33.6
13
+
14
+ # GymCraft Python RPC Client
15
+
16
+ Install dependencies:
17
+
18
+ ```powershell
19
+ cd src\main\python
20
+ uv sync
21
+ ```
22
+
23
+ Generate Python gRPC stubs from the mod proto files, then build the wheel:
24
+
25
+ ```powershell
26
+ # from repository root
27
+ .\gradlew generatePythonStubs
28
+ .\gradlew packagePython
29
+ ```
30
+
31
+ Or build directly with uv:
32
+
33
+ ```powershell
34
+ cd src\main\python
35
+ uv build
36
+ ```
37
+
38
+ Create an environment in-game with the env tool, then connect by entity UUID:
39
+
40
+ ```python
41
+ from gymcraft import GymCraftEnv
42
+ from gymcraft.gym.action.components import noop_pb2
43
+
44
+ env = GymCraftEnv("entity-uuid-here")
45
+ obs, reset_info = env.reset(options={
46
+ "disable_vanilla_ai": True,
47
+ "allow_multiple_actions": True,
48
+ }) # Disable vanilla AI only while waiting between actions; defaults to False.
49
+ self_state = obs["gymcraft:self"]
50
+
51
+ obs, reward, terminated, truncated, step_info = env.step({
52
+ "timeout_seconds": 0.0, # seconds; <= 0 means no limit
53
+ "actions": [
54
+ {"component_id": "gymcraft:noop", "payload": noop_pb2.ProtoNoop()},
55
+ ],
56
+ })
57
+ env.close()
58
+ ```
59
+
60
+ `reset()` returns `(observation, info)` and `step()` returns `(observation, reward, terminated, truncated, info)` (Gymnasium-style). The observation is the unpacked `Observation` dict: a `header` plus component keys that are full registration ids such as `gymcraft:self`, `gymcraft:nearby_blocks`, `gymcraft:menu`. A step receives an ordered `ActionBatch`; each action has exactly one `component_id` and protobuf `payload`, and the server executes the list serially under one shared `timeout_seconds`. `make_action()` packs one action into `ProtoMcAction`, while `unpack_observation()` converts a raw `ProtoMcObservation` back to an `Observation`.
61
+
62
+ `allow_multiple_actions` 默认为 `True`。在 reset options 中设为 `False` 后,每个 step 最多接受一个 action;多 action 批次会整体失败且不产生动作副作用,空批次 noop 仍可使用。
63
+
64
+ `gymcraft:update_interesting_blocks` 可通过 `add_block_ids` / `remove_block_ids` 批量维护当前 Agent 关注的方块类型;`gymcraft:interesting_blocks` 返回附近匹配类型的可见 `ProtoBlockView`。
65
+
66
+ Typecheck the Python client:
67
+
68
+ ```powershell
69
+ cd src\main\python
70
+ uv run mypy src debug demos tests
71
+ ```
72
+
73
+ GitHub Actions runs stub generation, mypy, packaging, and uploads the built distributions as the `gymcraft-python-dist` artifact.
74
+
75
+ ## LLM Agent 工具包
76
+
77
+ `gymcraft.llm` 将 LLM 相关能力拆成可独立复用的组件:
78
+
79
+ - `ObservationTextFormatter`:将 protobuf 观测转换为紧凑、稳定的行式文本;
80
+ - `ActionDslParser` / `encode_action_batch`:解析模型回复中的命令 DSL,并编码为现有 `Action`;
81
+ - `ConversationHistory` / `ContextAssembler`:组装通用 Chat Completions 消息;
82
+ - `LLMGymCraftEnv`:只负责串联上述组件与底层 `GymCraftEnv`,所有组件都可替换或单独使用。
83
+
84
+ 内置 system prompt、命令说明和动作纠错反馈均使用英文。观测只渲染决策所需的原生字段,不生成相对坐标等派生状态;附近实体、普通方块和感兴趣方块默认各最多 10 条。`action-result` 不包含服务端 `details`。
85
+ `ConversationHistory` 按 `history_turns` 分批保留完整历史;当前批次满载后,下一轮会折叠整批历史并作为新批次的第一轮重新积累,不再逐轮滚动淘汰最旧上下文。
86
+
87
+ 最小 wrapper 示例:
88
+
89
+ ````python
90
+ from gymcraft import GymCraftEnv
91
+ from gymcraft.llm import LLMGymCraftEnv
92
+
93
+ base_env = GymCraftEnv("entity-uuid-here")
94
+ env = LLMGymCraftEnv(base_env, task="走到最近的箱子旁并打开它。")
95
+ context, info = env.reset(options={"disable_vanilla_ai": True})
96
+
97
+ # context["messages"] 可直接交给任意 Chat Completions 兼容 API。
98
+ model_text = """我先靠近箱子。
99
+
100
+ ```gymcraft-action
101
+ /timeout 10
102
+ /move_to 12 64 -3 1
103
+ ```
104
+ """
105
+ context, reward, terminated, truncated, info = env.step(model_text)
106
+ env.close()
107
+ ````
108
+
109
+ 动作块位于回复末尾,每个非空行是一条 Minecraft 风格命令。一个动作块可以包含多个动作,服务端严格按文本行顺序串行执行,并允许重复同一组件。多条 `move_menu_item` 会保留为一个组件原生批量负载。调用 `ActionDslParser.command_reference()` 可以取得当前环境支持的完整命令表。
110
+
111
+ LLM 可用一条原子命令同时增删兴趣类型,例如 `/update_interesting_blocks add minecraft:diamond_ore mod:target_block remove minecraft:stone`。匹配结果会在后续观测的 `interesting_blocks` 段中按距离排序;其文本裁剪上限由 `ObservationFormatConfig.max_interesting_blocks` 控制。
112
+
113
+ 连接已创建的 `simple_mob` 环境进行真实 DSL/gRPC 调试:
114
+
115
+ ```powershell
116
+ uv run python debug/interesting_blocks_debug.py <entity_uuid> --add minecraft:diamond_ore
117
+ ```
118
+
119
+ 格式错误不会推进游戏状态。wrapper 会把错误和模型原文加入上下文,默认允许两次原地纠正,连续第三次非法输出会截断当前 rollout。服务端返回的 reward、terminated 和 truncated 不会被 Python 任务逻辑改写。
120
+
121
+ ## Chat Completions 闭环 Demo
122
+
123
+ Chat Completions 客户端是可选依赖:
124
+
125
+ ```powershell
126
+ uv sync --extra openai
127
+ ```
128
+
129
+ 运行 demo;`base_url`、API key 和模型均可替换为任意兼容服务提供的值:
130
+
131
+ ```powershell
132
+ $env:LLM_BASE_URL = "https://api.openai.com/v1"
133
+ $env:LLM_API_KEY = "your-api-key"
134
+ $env:LLM_MODEL = "your-model-id"
135
+
136
+ uv run --extra openai demos/llm_chat_completions_demo.py `
137
+ <entity_uuid> `
138
+ --task "找到最近的箱子并查看其中的物品"
139
+ ```
140
+
141
+ demo 使用通用的 `client.chat.completions.create(model=..., messages=...)` 接口,不启用 Responses API、厂商工具调用或服务端会话存储。
142
+
143
+ 连接 `gymcraft:iron_mining` 环境可运行完整生存工具链 demo:
144
+
145
+ ```powershell
146
+ uv run --extra openai demos/iron_mining_llm_demo.py `
147
+ <entity_uuid> `
148
+ --max-steps 64 `
149
+ --trace traces/iron-mining.jsonl
150
+ ```
151
+
152
+ 该 demo 内置从空手采集原木、使用 self 菜单 $2\times2$ 合成、放置工作台、制作石镐并拾取粗铁的任务提示。`--task` 可覆盖提示,`--trace` 可保存完整 JSONL 轨迹。
153
+
154
+ ## 人工终端交互
155
+
156
+ ```powershell
157
+ uv run debug/llm_terminal.py <entity_uuid> --task "测试菜单和移动动作"
158
+ ```
159
+
160
+ 终端中可直接输入一条或多条 `/command`,空行提交;工具会自动补上 `gymcraft-action` 围栏。输入 `:obs` 查看最新观测、`:context` 查看实际消息历史、`:quit` 退出。
161
+
162
+ ## 检查
163
+
164
+ ```powershell
165
+ uv run python -m unittest discover -s tests -v
166
+ uv run mypy src debug demos tests
167
+ ```
@@ -0,0 +1,154 @@
1
+ # GymCraft Python RPC Client
2
+
3
+ Install dependencies:
4
+
5
+ ```powershell
6
+ cd src\main\python
7
+ uv sync
8
+ ```
9
+
10
+ Generate Python gRPC stubs from the mod proto files, then build the wheel:
11
+
12
+ ```powershell
13
+ # from repository root
14
+ .\gradlew generatePythonStubs
15
+ .\gradlew packagePython
16
+ ```
17
+
18
+ Or build directly with uv:
19
+
20
+ ```powershell
21
+ cd src\main\python
22
+ uv build
23
+ ```
24
+
25
+ Create an environment in-game with the env tool, then connect by entity UUID:
26
+
27
+ ```python
28
+ from gymcraft import GymCraftEnv
29
+ from gymcraft.gym.action.components import noop_pb2
30
+
31
+ env = GymCraftEnv("entity-uuid-here")
32
+ obs, reset_info = env.reset(options={
33
+ "disable_vanilla_ai": True,
34
+ "allow_multiple_actions": True,
35
+ }) # Disable vanilla AI only while waiting between actions; defaults to False.
36
+ self_state = obs["gymcraft:self"]
37
+
38
+ obs, reward, terminated, truncated, step_info = env.step({
39
+ "timeout_seconds": 0.0, # seconds; <= 0 means no limit
40
+ "actions": [
41
+ {"component_id": "gymcraft:noop", "payload": noop_pb2.ProtoNoop()},
42
+ ],
43
+ })
44
+ env.close()
45
+ ```
46
+
47
+ `reset()` returns `(observation, info)` and `step()` returns `(observation, reward, terminated, truncated, info)` (Gymnasium-style). The observation is the unpacked `Observation` dict: a `header` plus component keys that are full registration ids such as `gymcraft:self`, `gymcraft:nearby_blocks`, `gymcraft:menu`. A step receives an ordered `ActionBatch`; each action has exactly one `component_id` and protobuf `payload`, and the server executes the list serially under one shared `timeout_seconds`. `make_action()` packs one action into `ProtoMcAction`, while `unpack_observation()` converts a raw `ProtoMcObservation` back to an `Observation`.
48
+
49
+ `allow_multiple_actions` 默认为 `True`。在 reset options 中设为 `False` 后,每个 step 最多接受一个 action;多 action 批次会整体失败且不产生动作副作用,空批次 noop 仍可使用。
50
+
51
+ `gymcraft:update_interesting_blocks` 可通过 `add_block_ids` / `remove_block_ids` 批量维护当前 Agent 关注的方块类型;`gymcraft:interesting_blocks` 返回附近匹配类型的可见 `ProtoBlockView`。
52
+
53
+ Typecheck the Python client:
54
+
55
+ ```powershell
56
+ cd src\main\python
57
+ uv run mypy src debug demos tests
58
+ ```
59
+
60
+ GitHub Actions runs stub generation, mypy, packaging, and uploads the built distributions as the `gymcraft-python-dist` artifact.
61
+
62
+ ## LLM Agent 工具包
63
+
64
+ `gymcraft.llm` 将 LLM 相关能力拆成可独立复用的组件:
65
+
66
+ - `ObservationTextFormatter`:将 protobuf 观测转换为紧凑、稳定的行式文本;
67
+ - `ActionDslParser` / `encode_action_batch`:解析模型回复中的命令 DSL,并编码为现有 `Action`;
68
+ - `ConversationHistory` / `ContextAssembler`:组装通用 Chat Completions 消息;
69
+ - `LLMGymCraftEnv`:只负责串联上述组件与底层 `GymCraftEnv`,所有组件都可替换或单独使用。
70
+
71
+ 内置 system prompt、命令说明和动作纠错反馈均使用英文。观测只渲染决策所需的原生字段,不生成相对坐标等派生状态;附近实体、普通方块和感兴趣方块默认各最多 10 条。`action-result` 不包含服务端 `details`。
72
+ `ConversationHistory` 按 `history_turns` 分批保留完整历史;当前批次满载后,下一轮会折叠整批历史并作为新批次的第一轮重新积累,不再逐轮滚动淘汰最旧上下文。
73
+
74
+ 最小 wrapper 示例:
75
+
76
+ ````python
77
+ from gymcraft import GymCraftEnv
78
+ from gymcraft.llm import LLMGymCraftEnv
79
+
80
+ base_env = GymCraftEnv("entity-uuid-here")
81
+ env = LLMGymCraftEnv(base_env, task="走到最近的箱子旁并打开它。")
82
+ context, info = env.reset(options={"disable_vanilla_ai": True})
83
+
84
+ # context["messages"] 可直接交给任意 Chat Completions 兼容 API。
85
+ model_text = """我先靠近箱子。
86
+
87
+ ```gymcraft-action
88
+ /timeout 10
89
+ /move_to 12 64 -3 1
90
+ ```
91
+ """
92
+ context, reward, terminated, truncated, info = env.step(model_text)
93
+ env.close()
94
+ ````
95
+
96
+ 动作块位于回复末尾,每个非空行是一条 Minecraft 风格命令。一个动作块可以包含多个动作,服务端严格按文本行顺序串行执行,并允许重复同一组件。多条 `move_menu_item` 会保留为一个组件原生批量负载。调用 `ActionDslParser.command_reference()` 可以取得当前环境支持的完整命令表。
97
+
98
+ LLM 可用一条原子命令同时增删兴趣类型,例如 `/update_interesting_blocks add minecraft:diamond_ore mod:target_block remove minecraft:stone`。匹配结果会在后续观测的 `interesting_blocks` 段中按距离排序;其文本裁剪上限由 `ObservationFormatConfig.max_interesting_blocks` 控制。
99
+
100
+ 连接已创建的 `simple_mob` 环境进行真实 DSL/gRPC 调试:
101
+
102
+ ```powershell
103
+ uv run python debug/interesting_blocks_debug.py <entity_uuid> --add minecraft:diamond_ore
104
+ ```
105
+
106
+ 格式错误不会推进游戏状态。wrapper 会把错误和模型原文加入上下文,默认允许两次原地纠正,连续第三次非法输出会截断当前 rollout。服务端返回的 reward、terminated 和 truncated 不会被 Python 任务逻辑改写。
107
+
108
+ ## Chat Completions 闭环 Demo
109
+
110
+ Chat Completions 客户端是可选依赖:
111
+
112
+ ```powershell
113
+ uv sync --extra openai
114
+ ```
115
+
116
+ 运行 demo;`base_url`、API key 和模型均可替换为任意兼容服务提供的值:
117
+
118
+ ```powershell
119
+ $env:LLM_BASE_URL = "https://api.openai.com/v1"
120
+ $env:LLM_API_KEY = "your-api-key"
121
+ $env:LLM_MODEL = "your-model-id"
122
+
123
+ uv run --extra openai demos/llm_chat_completions_demo.py `
124
+ <entity_uuid> `
125
+ --task "找到最近的箱子并查看其中的物品"
126
+ ```
127
+
128
+ demo 使用通用的 `client.chat.completions.create(model=..., messages=...)` 接口,不启用 Responses API、厂商工具调用或服务端会话存储。
129
+
130
+ 连接 `gymcraft:iron_mining` 环境可运行完整生存工具链 demo:
131
+
132
+ ```powershell
133
+ uv run --extra openai demos/iron_mining_llm_demo.py `
134
+ <entity_uuid> `
135
+ --max-steps 64 `
136
+ --trace traces/iron-mining.jsonl
137
+ ```
138
+
139
+ 该 demo 内置从空手采集原木、使用 self 菜单 $2\times2$ 合成、放置工作台、制作石镐并拾取粗铁的任务提示。`--task` 可覆盖提示,`--trace` 可保存完整 JSONL 轨迹。
140
+
141
+ ## 人工终端交互
142
+
143
+ ```powershell
144
+ uv run debug/llm_terminal.py <entity_uuid> --task "测试菜单和移动动作"
145
+ ```
146
+
147
+ 终端中可直接输入一条或多条 `/command`,空行提交;工具会自动补上 `gymcraft-action` 围栏。输入 `:obs` 查看最新观测、`:context` 查看实际消息历史、`:quit` 退出。
148
+
149
+ ## 检查
150
+
151
+ ```powershell
152
+ uv run python -m unittest discover -s tests -v
153
+ uv run mypy src debug demos tests
154
+ ```
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75", "grpcio-tools>=1.81.1"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gymcraft"
7
+ version = "1.2.0" # 这里和模组版本号保持一致
8
+ description = "Python RPC client for GymCraft - Gymnasium-style RL environments in Minecraft"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "grpcio>=1.81.1",
13
+ "gymnasium>=1.3.0",
14
+ "mcrcon>=0.7.0",
15
+ "mypy>=2.1.0",
16
+ "openai>=3.3.1",
17
+ "protobuf>=6.33.6",
18
+ ]
19
+
20
+ [dependency-groups]
21
+ dev = [
22
+ "grpcio-tools>=1.81.1",
23
+ "mcp>=2.1.1",
24
+ "openai>=2.0.0",
25
+ "python-dotenv>=1.2.3",
26
+ ]
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+ include = ["gymcraft", "gymcraft.*"]
31
+
32
+ [tool.mypy]
33
+ python_version = "3.11"
34
+ files = ["src/gymcraft", "debug"]
35
+ exclude = ["src/gymcraft/gym/"]
36
+ mypy_path = "src"
37
+
38
+ warn_return_any = true
39
+ warn_unused_configs = true
40
+ warn_unused_ignores = true
41
+ warn_redundant_casts = true
42
+ warn_unreachable = true
43
+
44
+ disallow_untyped_defs = true
45
+ disallow_incomplete_defs = true
46
+ check_untyped_defs = true
47
+ disallow_untyped_decorators = true
48
+
49
+ no_implicit_optional = true
50
+ strict_equality = true
51
+ show_error_codes = true
52
+ pretty = true
53
+
54
+ [[tool.mypy.overrides]]
55
+ module = [
56
+ "gymcraft.gym.*",
57
+ ]
58
+ follow_imports = "skip"
59
+ ignore_errors = true
60
+
61
+ [[tool.mypy.overrides]]
62
+ module = [
63
+ "grpc.*",
64
+ "gymnasium.*",
65
+ "google.*",
66
+ ]
67
+ ignore_missing_imports = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ from gymcraft.client import GymCraftEnv, make_action, make_step_request, single_action, unpack_observation
2
+ from gymcraft.llm import LLMEnvConfig, LLMGymCraftEnv
3
+
4
+ __all__ = [
5
+ "GymCraftEnv",
6
+ "LLMEnvConfig",
7
+ "LLMGymCraftEnv",
8
+ "make_action",
9
+ "make_step_request",
10
+ "single_action",
11
+ "unpack_observation",
12
+ ]
@@ -0,0 +1,159 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Mapping
5
+ from typing import Any, Tuple, TypeVar, cast
6
+
7
+ import grpc
8
+ import gymnasium as gym
9
+ from google.protobuf import message
10
+
11
+ from gymcraft.gym.action.action_pb2 import ProtoMcAction
12
+ from gymcraft.gym.observation.components.chat_pb2 import ProtoRecentChat
13
+ from gymcraft.gym.observation.components.menu_pb2 import ProtoMenuObservation
14
+ from gymcraft.gym.observation.components.interesting_blocks_pb2 import ProtoInterestingBlocks
15
+ from gymcraft.gym.observation.components.nearby_blocks_pb2 import ProtoNearbyBlocks
16
+ from gymcraft.gym.observation.components.nearby_entities_pb2 import ProtoNearbyEntities
17
+ from gymcraft.gym.observation.components.nearby_items_pb2 import ProtoNearbyItems
18
+ from gymcraft.gym.observation.components.self_pb2 import ProtoSelfState
19
+ from gymcraft.gym.observation.components.world_pb2 import ProtoWorldState
20
+ from gymcraft.gym.observation.observation_pb2 import ProtoMcObservation
21
+ from gymcraft.gym.rpc.env_service_pb2 import (
22
+ CloseSessionRequest,
23
+ ConnectRequest,
24
+ ResetRequest,
25
+ ResetResponse,
26
+ StepRequest,
27
+ StepResponse,
28
+ )
29
+ from gymcraft.gym.rpc.env_service_pb2_grpc import GymEnvServiceStub
30
+ from gymcraft.type_info import (
31
+ Action,
32
+ ActionBatch,
33
+ Observation,
34
+ OBS_INTERESTING_BLOCKS,
35
+ OBS_MENU,
36
+ OBS_NEARBY_BLOCKS,
37
+ OBS_NEARBY_ENTITIES,
38
+ OBS_NEARBY_ITEMS,
39
+ OBS_CHAT,
40
+ OBS_SELF,
41
+ OBS_WORLD,
42
+ TIMEOUT_SECONDS,
43
+ )
44
+
45
+ _TMessage = TypeVar("_TMessage", bound=message.Message)
46
+
47
+ # 观测组件注册 id(完整 wire 键,含 `gymcraft:` 命名空间)→ 解包目标 protobuf 消息类型。
48
+ # 新增观测组件时需在此登记,否则 `unpack_observation` 会将其视为未知组件。
49
+ _OBSERVATION_COMPONENT_TYPES: dict[str, type[message.Message]] = {
50
+ OBS_SELF: ProtoSelfState,
51
+ OBS_WORLD: ProtoWorldState,
52
+ OBS_NEARBY_ENTITIES: ProtoNearbyEntities,
53
+ OBS_NEARBY_BLOCKS: ProtoNearbyBlocks,
54
+ OBS_MENU: ProtoMenuObservation,
55
+ OBS_NEARBY_ITEMS: ProtoNearbyItems,
56
+ OBS_INTERESTING_BLOCKS: ProtoInterestingBlocks,
57
+ OBS_CHAT: ProtoRecentChat,
58
+ }
59
+
60
+
61
+ class GymCraftEnv(gym.Env[Any, Any]):
62
+ metadata = {"render_modes": []}
63
+
64
+ def __init__(self, entity_uuid: str, address: str = "localhost:50051") -> None:
65
+ self.entity_uuid = entity_uuid
66
+ self.address = address
67
+ self.channel = grpc.insecure_channel(address)
68
+ self.stub = GymEnvServiceStub(self.channel)
69
+
70
+ response = self.stub.Connect(ConnectRequest(entity_uuid=entity_uuid))
71
+ self.session_id = response.session_id
72
+ self.entity_uuid = response.entity_uuid
73
+ self.remote_metadata = json.loads(response.metadata)
74
+ self.action_space_spec = json.loads(response.action_space_json)
75
+ self.observation_space_spec = json.loads(response.observation_space_json)
76
+
77
+ # gymnasium 约定 info 为 dict,本环境按 proto 直接透传 info 的 JSON 字符串。
78
+ def reset(self, *, seed: int | None = None, options: Mapping[str, Any] | None = None) -> Tuple[Observation, str]: # type: ignore[override] # noqa: E501
79
+ super().reset(seed=seed)
80
+ request = ResetRequest(session_id=self.session_id)
81
+ if seed is not None:
82
+ request.seed = seed
83
+ request.options = json.dumps(options or {})
84
+
85
+ response = self.stub.Reset(request)
86
+ assert isinstance(response, ResetResponse)
87
+ return unpack_observation(response.observation), response.info
88
+
89
+ # gymnasium 约定 info 为 dict,本环境按 proto 直接透传 info 的 JSON 字符串。
90
+ def step(self, action: ActionBatch) -> Tuple[Observation, float, bool, bool, str]: # type: ignore[override] # noqa: E501
91
+ request = make_step_request(self.session_id, action)
92
+ response = self.stub.Step(request)
93
+ assert isinstance(response, StepResponse)
94
+ return (
95
+ unpack_observation(response.observation),
96
+ response.reward,
97
+ response.terminated,
98
+ response.truncated,
99
+ response.info,
100
+ )
101
+
102
+ def close(self) -> None:
103
+ session_id = getattr(self, "session_id", None)
104
+ if session_id:
105
+ self.stub.CloseSession(CloseSessionRequest(session_id=session_id))
106
+ self.session_id = ""
107
+ self.channel.close()
108
+
109
+
110
+ def make_action(action: Action) -> ProtoMcAction:
111
+ """把单个 ``component_id`` / ``payload`` 动作打包为 wire 消息。"""
112
+ proto_action = ProtoMcAction(component_id=action["component_id"])
113
+ proto_action.payload.Pack(cast(message.Message, action["payload"]))
114
+ return proto_action
115
+
116
+
117
+ def single_action(component_id: str, payload: message.Message, timeout_seconds: float = 0.0) -> ActionBatch:
118
+ """构造只包含一项的 step 动作批次。"""
119
+ return {
120
+ "timeout_seconds": timeout_seconds,
121
+ "actions": [{"component_id": component_id, "payload": payload}],
122
+ }
123
+
124
+
125
+ def make_step_request(session_id: str, batch: ActionBatch) -> StepRequest:
126
+ """把用户视角的动作批次打包为 gRPC StepRequest。"""
127
+ return StepRequest(
128
+ session_id=session_id,
129
+ actions=[make_action(item) for item in batch["actions"]],
130
+ timeout_seconds=batch.get(TIMEOUT_SECONDS, 0.0),
131
+ )
132
+
133
+
134
+ def _unpack_component(
135
+ raw_observation: ProtoMcObservation,
136
+ key: str,
137
+ message_type: type[_TMessage],
138
+ ) -> _TMessage:
139
+ packed = raw_observation.components[key]
140
+ payload = cast(_TMessage, message_type())
141
+ if not packed.Unpack(payload):
142
+ raise ValueError(f"观测组件 {key!r} 无法解包为 {message_type.DESCRIPTOR.full_name}")
143
+ return payload
144
+
145
+
146
+ def unpack_observation(raw_observation: ProtoMcObservation) -> Observation:
147
+ """把 wire 上的 ``ProtoMcObservation`` 解包为用户视角的 ``Observation``。
148
+
149
+ ``header`` 原样保留;``components`` 中已登记的组件按注册 id 解包为对应的
150
+ protobuf 消息,未登记的键抛 ``ValueError``,避免静默丢失新组件。
151
+ """
152
+ unknown = set(raw_observation.components) - set(_OBSERVATION_COMPONENT_TYPES)
153
+ if unknown:
154
+ raise ValueError(f"观测包含未识别的组件: {sorted(unknown)}")
155
+ result: dict[str, Any] = {"header": raw_observation.header}
156
+ for key, message_type in _OBSERVATION_COMPONENT_TYPES.items():
157
+ if key in raw_observation.components:
158
+ result[key] = _unpack_component(raw_observation, key, message_type)
159
+ return cast(Observation, result)
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: gymcraft/gym/action/action.proto
5
+ # Protobuf Python Version: 6.33.5
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 33,
16
+ 5,
17
+ '',
18
+ 'gymcraft/gym/action/action.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+ from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2
26
+
27
+
28
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n gymcraft/gym/action/action.proto\x12\x13gymcraft.gym.action\x1a\x19google/protobuf/any.proto\"u\n\rProtoMcAction\x12\x14\n\x0c\x63omponent_id\x18\x03 \x01(\t\x12%\n\x07payload\x18\x04 \x01(\x0b\x32\x14.google.protobuf.AnyJ\x04\x08\x01\x10\x02J\x04\x08\x02\x10\x03R\ncomponentsR\x0ftimeout_secondsB>\n-io.github.mousemeya.gymcraft.gym.action.protoB\x0b\x41\x63tionProtoP\x01\x62\x06proto3')
29
+
30
+ _globals = globals()
31
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
32
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gymcraft.gym.action.action_pb2', _globals)
33
+ if not _descriptor._USE_C_DESCRIPTORS:
34
+ _globals['DESCRIPTOR']._loaded_options = None
35
+ _globals['DESCRIPTOR']._serialized_options = b'\n-io.github.mousemeya.gymcraft.gym.action.protoB\013ActionProtoP\001'
36
+ _globals['_PROTOMCACTION']._serialized_start=84
37
+ _globals['_PROTOMCACTION']._serialized_end=201
38
+ # @@protoc_insertion_point(module_scope)
@@ -0,0 +1,15 @@
1
+ from google.protobuf import any_pb2 as _any_pb2
2
+ from google.protobuf import descriptor as _descriptor
3
+ from google.protobuf import message as _message
4
+ from collections.abc import Mapping as _Mapping
5
+ from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
6
+
7
+ DESCRIPTOR: _descriptor.FileDescriptor
8
+
9
+ class ProtoMcAction(_message.Message):
10
+ __slots__ = ("component_id", "payload")
11
+ COMPONENT_ID_FIELD_NUMBER: _ClassVar[int]
12
+ PAYLOAD_FIELD_NUMBER: _ClassVar[int]
13
+ component_id: str
14
+ payload: _any_pb2.Any
15
+ def __init__(self, component_id: _Optional[str] = ..., payload: _Optional[_Union[_any_pb2.Any, _Mapping]] = ...) -> None: ...
@@ -0,0 +1,24 @@
1
+ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
2
+ """Client and server classes corresponding to protobuf-defined services."""
3
+ import grpc
4
+ import warnings
5
+
6
+
7
+ GRPC_GENERATED_VERSION = '1.81.1'
8
+ GRPC_VERSION = grpc.__version__
9
+ _version_not_supported = False
10
+
11
+ try:
12
+ from grpc._utilities import first_version_is_lower
13
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
14
+ except ImportError:
15
+ _version_not_supported = True
16
+
17
+ if _version_not_supported:
18
+ raise RuntimeError(
19
+ f'The grpc package installed is at version {GRPC_VERSION},'
20
+ + ' but the generated code in gymcraft/gym/action/action_pb2_grpc.py depends on'
21
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
22
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
23
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
24
+ )
@@ -0,0 +1,37 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # NO CHECKED-IN PROTOBUF GENCODE
4
+ # source: gymcraft/gym/action/components/attack_once.proto
5
+ # Protobuf Python Version: 6.33.5
6
+ """Generated protocol buffer code."""
7
+ from google.protobuf import descriptor as _descriptor
8
+ from google.protobuf import descriptor_pool as _descriptor_pool
9
+ from google.protobuf import runtime_version as _runtime_version
10
+ from google.protobuf import symbol_database as _symbol_database
11
+ from google.protobuf.internal import builder as _builder
12
+ _runtime_version.ValidateProtobufRuntimeVersion(
13
+ _runtime_version.Domain.PUBLIC,
14
+ 6,
15
+ 33,
16
+ 5,
17
+ '',
18
+ 'gymcraft/gym/action/components/attack_once.proto'
19
+ )
20
+ # @@protoc_insertion_point(imports)
21
+
22
+ _sym_db = _symbol_database.Default()
23
+
24
+
25
+
26
+
27
+ DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n0gymcraft/gym/action/components/attack_once.proto\x12\x13gymcraft.gym.action\"+\n\x0fProtoAttackOnce\x12\x18\n\x10target_entity_id\x18\x01 \x01(\x05\x42\x42\n-io.github.mousemeya.gymcraft.gym.action.protoB\x0f\x41ttackOnceProtoP\x01\x62\x06proto3')
28
+
29
+ _globals = globals()
30
+ _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
31
+ _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gymcraft.gym.action.components.attack_once_pb2', _globals)
32
+ if not _descriptor._USE_C_DESCRIPTORS:
33
+ _globals['DESCRIPTOR']._loaded_options = None
34
+ _globals['DESCRIPTOR']._serialized_options = b'\n-io.github.mousemeya.gymcraft.gym.action.protoB\017AttackOnceProtoP\001'
35
+ _globals['_PROTOATTACKONCE']._serialized_start=73
36
+ _globals['_PROTOATTACKONCE']._serialized_end=116
37
+ # @@protoc_insertion_point(module_scope)