plancraft 0.4.2__py3-none-any.whl → 0.4.3__py3-none-any.whl

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.
@@ -323,12 +323,11 @@ class PlancraftEnvironment:
323
323
  # not enough
324
324
  if self.slot_empty(slot_from) or self.state[slot_from]["quantity"] < quantity:
325
325
  return
326
- # if craft slot - must take all
326
+ # if craft slot - move takes all
327
327
  if slot_from == 0 and self.state[slot_from]["quantity"] != quantity:
328
- return
328
+ quantity = self.state[slot_from]["quantity"]
329
329
 
330
330
  item = self.state[slot_from]
331
-
332
331
  # slot to is not empty or is the same type as item
333
332
  if self.slot_empty(slot_to):
334
333
  # add quantity to new slot
@@ -110,17 +110,21 @@ def sample_recipes(
110
110
  return start_inputs, overall_exclude_set
111
111
 
112
112
 
113
- def remove_ancestor_items(target: str, inventory: dict[str, int]) -> dict[str, int]:
113
+ def remove_ancestor_items(
114
+ target: str, inventory: dict[str, int]
115
+ ) -> tuple[dict[str, int], list[tuple[str, int]]]:
114
116
  ancestors = set(get_ancestors(target))
115
117
  possible_items = set(inventory.keys())
116
118
  items_to_remove = list(ancestors.intersection(possible_items))
117
119
  num_items = random.randint(1, len(items_to_remove))
120
+ removed_items = []
118
121
  for item in random.sample(items_to_remove, num_items):
119
122
  count_to_remove = random.randint(1, inventory[item])
120
123
  inventory[item] -= count_to_remove
121
124
  if inventory[item] == 0:
122
125
  del inventory[item]
123
- return inventory
126
+ removed_items.append((item, count_to_remove))
127
+ return inventory, removed_items
124
128
 
125
129
 
126
130
  def construct_example(
@@ -142,9 +146,10 @@ def construct_example(
142
146
 
143
147
  # sample the recipe
144
148
  inventory, overall_exclude_set = sample_recipes(target, set())
149
+ removed_items = []
145
150
  if impossible:
146
151
  # if impossible then remove one or more items from the inventory
147
- inventory = remove_ancestor_items(
152
+ inventory, removed_items = remove_ancestor_items(
148
153
  target,
149
154
  inventory,
150
155
  )
@@ -158,7 +163,7 @@ def construct_example(
158
163
  while (optimal_path is not None and impossible) or (
159
164
  optimal_path is None and not impossible
160
165
  ):
161
- inventory = remove_ancestor_items(target, inventory)
166
+ inventory, removed_items = remove_ancestor_items(target, inventory)
162
167
  optimal_path = optimal_planner(target, inventory)
163
168
 
164
169
  # assign to slots
@@ -169,6 +174,7 @@ def construct_example(
169
174
  "target": target,
170
175
  "num_distractors": num_distractors,
171
176
  "impossible": impossible,
177
+ "missing_items": removed_items,
172
178
  }
173
179
  # either impossible and no path or not impossible and path exists
174
180
  assert (impossible and optimal_path is None) or (
plancraft/mcp.py ADDED
@@ -0,0 +1,429 @@
1
+ import base64
2
+ import csv
3
+ import os
4
+ import random
5
+ from collections.abc import AsyncIterator
6
+ from contextlib import asynccontextmanager
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Literal, Optional
9
+
10
+ from loguru import logger
11
+ from PIL import Image as PILImage
12
+ from plancraft.config import PlancraftExample
13
+ from plancraft.environment.actions import (
14
+ MoveAction,
15
+ SmeltAction,
16
+ StopAction,
17
+ )
18
+ from plancraft.environment.env import (
19
+ PlancraftEnvironment,
20
+ get_objective_str,
21
+ target_and_inventory_to_text_obs,
22
+ )
23
+ from plancraft.simple import get_plancraft_examples
24
+
25
+ from mcp.server.fastmcp import Context, FastMCP
26
+ from mcp.types import CallToolResult, ImageContent, TextContent
27
+
28
+
29
+ class PlancraftMCPWrapper:
30
+ def __init__(
31
+ self,
32
+ example: PlancraftExample,
33
+ max_steps: int = 30,
34
+ resolution: str = "high",
35
+ use_text_inventory: bool = True,
36
+ ):
37
+ self.max_steps = max_steps
38
+ # whether to convert the inventory to text observation
39
+ # if False, only the objective string is returned
40
+ self.use_text_inventory = use_text_inventory
41
+ self.current_step = 0
42
+ self.stopped = False
43
+ self.success = False
44
+ self.example = example
45
+ self.resolution = resolution
46
+ self.environment = PlancraftEnvironment(
47
+ example.slotted_inventory, resolution=self.resolution
48
+ )
49
+
50
+ def check_done(self, inventory: dict, target: str):
51
+ """
52
+ Check that target object is obtained
53
+ """
54
+ for slot, item in inventory.items():
55
+ # ensure the target is in the inventory (not in slot 0)
56
+ if target == item["type"] and slot != 0:
57
+ return True
58
+ return False
59
+
60
+ def step(
61
+ self, action: Optional[StopAction | MoveAction | SmeltAction] = None
62
+ ) -> tuple[dict[str, Any], bool]:
63
+ # Handle already stopped case
64
+ if self.stopped:
65
+ return (
66
+ {"text": "Plancraft environment is terminated"},
67
+ True,
68
+ )
69
+
70
+ # Handle initial step
71
+ if not action:
72
+ observation = self.environment.step()
73
+ observation["target"] = self.example.target
74
+ if self.use_text_inventory:
75
+ text = target_and_inventory_to_text_obs(
76
+ target=self.example.target, inventory=observation["inventory"]
77
+ )
78
+ else:
79
+ text = get_objective_str(self.example.target)
80
+ observation["text"] = text
81
+ return observation, self.stopped
82
+
83
+ # Handle max steps reached
84
+ if self.current_step > self.max_steps:
85
+ self.stopped = True
86
+ return (
87
+ {"text": f"Max steps ({self.max_steps}) reached"},
88
+ self.stopped,
89
+ )
90
+
91
+ self.current_step += 1
92
+ # Handle stop action
93
+ if isinstance(action, StopAction):
94
+ self.stopped = True
95
+ # success is True if example was truly impossible
96
+ self.success = self.example.impossible
97
+ observation = {
98
+ "text": "Plancraft environment is terminate due to stop action"
99
+ }
100
+ else:
101
+ observation = self.environment.step(action)
102
+ observation["target"] = self.example.target
103
+
104
+ # Generate text observation
105
+ if self.use_text_inventory:
106
+ text = target_and_inventory_to_text_obs(
107
+ target=self.example.target, inventory=observation["inventory"]
108
+ )
109
+ else:
110
+ text = get_objective_str(self.example.target)
111
+
112
+ observation["text"] = text
113
+
114
+ self.success = self.check_done(
115
+ observation["inventory"], self.example.target
116
+ )
117
+
118
+ return (
119
+ observation,
120
+ self.stopped,
121
+ )
122
+
123
+
124
+ @dataclass
125
+ class PlancraftContext:
126
+ """Context for the Plancraft environment."""
127
+
128
+ env: Optional[Any] = None
129
+ examples: list[PlancraftExample] = field(default_factory=list)
130
+
131
+ # Default environment settings
132
+ max_steps: int = 30
133
+ resolution: Literal["high", "low"] = "high"
134
+ use_text_inventory: bool = True
135
+ terminated: bool = False
136
+ use_images: bool = False
137
+
138
+ # history and statistics
139
+ history: list[dict] = field(default_factory=list)
140
+
141
+
142
+ @asynccontextmanager
143
+ async def app_lifespan(server: FastMCP) -> AsyncIterator[PlancraftContext]:
144
+ """Manage application lifecycle with type-safe context"""
145
+ # Initialize on startup
146
+ try:
147
+ logger.info("Starting up")
148
+ examples = get_plancraft_examples(split="train")
149
+ yield PlancraftContext(examples=examples)
150
+ finally:
151
+ # Cleanup on shutdown
152
+ logger.info("Shutting down")
153
+
154
+
155
+ # Pass lifespan to server
156
+ app = FastMCP("Plancraft", lifespan=app_lifespan)
157
+
158
+
159
+ @app.prompt()
160
+ def plancraft_environment_instructions() -> str:
161
+ return """You are crafting in Minecraft. You need to decide on the next action.
162
+
163
+ Crafting Grid: The crafting table is organized into a 3x3 grid. Each slot in the grid has a unique identifier:
164
+ - Top row: [A1] [A2] [A3]
165
+ - Middle row: [B1] [B2] [B3]
166
+ - Bottom row: [C1] [C2] [C3]
167
+
168
+ The output of the crafting process is placed in a designated output slot labeled [0] You cannot move or smelt items directly into slot [0]
169
+
170
+ Inventory Slots: The remaining inventory slots (outside of the crafting grid) are used for storing items. These slots are labeled as [I1] to [I36]
171
+
172
+ Constraints:
173
+ - You cannot move or smelt items into [0]
174
+ - If an item is not in slot [0] then the recipe is incorrect
175
+ - You need to move items from [0] to a free inventory slot to complete the crafting process"""
176
+
177
+
178
+ def observation_to_tool_result(
179
+ observation: dict,
180
+ terminated: bool = False,
181
+ success: bool = False,
182
+ add_instructions=False,
183
+ use_images=False,
184
+ ) -> CallToolResult:
185
+ content = []
186
+ if add_instructions:
187
+ instructions = plancraft_environment_instructions()
188
+ content.append(TextContent(text=instructions, type="text"))
189
+
190
+ text_content = observation["text"]
191
+ # Add success message if the task was completed successfully
192
+ if success:
193
+ text_content = f"SUCCESS! You have completed the task: {text_content}"
194
+ # Add termination message if the task was terminated but not successful
195
+ elif terminated:
196
+ text_content = f"Task terminated: {text_content}"
197
+
198
+ content.append(TextContent(text=text_content, type="text"))
199
+
200
+ if use_images:
201
+ # numpy array to PIL
202
+ pil_image = PILImage.fromarray(observation["image"])
203
+ # Save the image to a BytesIO buffer in PNG format
204
+ import io
205
+
206
+ buffer = io.BytesIO()
207
+ pil_image.save(buffer, format="PNG")
208
+ buffer.seek(0)
209
+
210
+ # Encode the properly formatted PNG data
211
+ base64_data = base64.b64encode(buffer.getvalue()).decode("utf-8")
212
+ content.append(
213
+ ImageContent(type="image", data=base64_data, mimeType="image/png")
214
+ )
215
+ return CallToolResult(
216
+ content=content,
217
+ )
218
+
219
+
220
+ def add_action_to_history(
221
+ action: MoveAction | SmeltAction | StopAction, result: CallToolResult, ctx: Context
222
+ ) -> None:
223
+ """Add action call to history - storing only text content for simplicity"""
224
+ # Extract only the text content from the result
225
+ text_content = ""
226
+ for content in result.content:
227
+ if hasattr(content, "text"):
228
+ text_content = content.text
229
+ break
230
+ ctx.request_context.lifespan_context.history.append(
231
+ {"action": str(action), "observation": text_content}
232
+ )
233
+
234
+
235
+ def save_result_if_terminated(environment, terminated: bool, ctx: Context) -> None:
236
+ """Helper function to save results if the environment is terminated"""
237
+ if terminated:
238
+ logger.info(f"Task completed or terminated in {environment.current_step} steps")
239
+
240
+ result_data = {
241
+ "example_id": environment.example.id,
242
+ "success": environment.success,
243
+ "steps": environment.current_step,
244
+ "history": ctx.request_context.lifespan_context.history.copy(),
245
+ }
246
+
247
+ # save the result data to a CSV file
248
+ pwd = os.path.dirname(os.path.abspath(__file__))
249
+ os.makedirs("results", exist_ok=True)
250
+ result_file = os.path.join(pwd, "results", "results.csv")
251
+ with open(result_file, mode="a", newline="") as f:
252
+ writer = csv.DictWriter(f, fieldnames=result_data.keys())
253
+ if f.tell() == 0:
254
+ writer.writeheader()
255
+ writer.writerow(result_data)
256
+
257
+ # Reset history after storing results
258
+ ctx.request_context.lifespan_context.history = []
259
+
260
+
261
+ def correct_slot_format(slot: str):
262
+ # helper function to correct the slot format
263
+ # Claude seems unable to generate slots with brackets
264
+ if "[" not in slot and "]" not in slot:
265
+ return f"[{slot}]"
266
+
267
+
268
+ @app.tool(
269
+ name="smelt",
270
+ description="Smelt items in the Plancraft environment. You must specify the slot to smelt from and the slot to smelt to and quantity.",
271
+ )
272
+ def smelt(from_slot: str, to_slot: str, quantity: int, ctx: Context) -> CallToolResult:
273
+ """Smelt items in the Plancraft environment. You must specify the slot to smelt from and the slot to smelt to and quantity."""
274
+ try:
275
+ environment = ctx.request_context.lifespan_context.env
276
+ use_images = ctx.request_context.lifespan_context.use_images
277
+
278
+ if environment.stopped:
279
+ content = "Plancraft environment is terminated, first call start_new_task to start a new task"
280
+ return CallToolResult(
281
+ content=[TextContent(text=content, type="text")], isError=True
282
+ )
283
+
284
+ smelt_action = SmeltAction(
285
+ slot_from=correct_slot_format(from_slot),
286
+ slot_to=correct_slot_format(to_slot),
287
+ quantity=quantity,
288
+ )
289
+ obs, terminated = environment.step(smelt_action)
290
+
291
+ # Generate the result with appropriate success/termination messages
292
+ result = observation_to_tool_result(
293
+ obs,
294
+ terminated=terminated,
295
+ success=environment.success,
296
+ use_images=use_images,
297
+ )
298
+ add_action_to_history(smelt_action, result, ctx)
299
+ logger.info(f"Step {environment.current_step}: {obs['text']}")
300
+
301
+ # Save result if the task is terminated
302
+ save_result_if_terminated(environment, terminated, ctx)
303
+
304
+ return result
305
+ except Exception as e:
306
+ return CallToolResult(
307
+ content=[TextContent(text=str(e), type="text")], isError=True
308
+ )
309
+
310
+
311
+ @app.tool(
312
+ name="move",
313
+ description="Move items in the Plancraft environment. You must specify the slot to move from and the slot to move to and quantity.",
314
+ )
315
+ def move(from_slot: str, to_slot: str, quantity: int, ctx: Context) -> CallToolResult:
316
+ """
317
+ Move items in the Plancraft environment. You must specify the slot to move from and the slot to move to and quantity.
318
+ """
319
+ try:
320
+ environment = ctx.request_context.lifespan_context.env
321
+ use_images = ctx.request_context.lifespan_context.use_images
322
+ if environment.stopped:
323
+ content = "Plancraft environment is terminated, first call start_new_task to start a new task"
324
+ return CallToolResult(
325
+ content=[TextContent(text=content, type="text")], isError=True
326
+ )
327
+
328
+ move_action = MoveAction(
329
+ slot_from=correct_slot_format(from_slot),
330
+ slot_to=correct_slot_format(to_slot),
331
+ quantity=quantity,
332
+ )
333
+ obs, terminated = environment.step(move_action)
334
+
335
+ # Generresult with appropriate success/termination messages
336
+ result = observation_to_tool_result(
337
+ obs,
338
+ terminated=terminated,
339
+ success=environment.success,
340
+ use_images=use_images,
341
+ )
342
+ add_action_to_history(move_action, result, ctx)
343
+ logger.info(f"Step {environment.current_step}: {obs['text']}")
344
+
345
+ # Save result if the task is terminated
346
+ save_result_if_terminated(environment, terminated, ctx)
347
+
348
+ return result
349
+
350
+ except Exception as e:
351
+ return CallToolResult(
352
+ content=[TextContent(text=str(e), type="text")], isError=True
353
+ )
354
+
355
+
356
+ @app.tool(
357
+ name="impossible",
358
+ description="Declare the current task impossible. This will end the current task.",
359
+ )
360
+ def impossible_task(reason: str, ctx: Context) -> CallToolResult:
361
+ """Declare the current task impossible. This will end the current task."""
362
+ try:
363
+ environment = ctx.request_context.lifespan_context.env
364
+ use_images = ctx.request_context.lifespan_context.use_images
365
+ stop_action = StopAction(reason=reason)
366
+ obs, terminated = environment.step(stop_action)
367
+
368
+ # Generate the ith appropriate success/termination messages
369
+ # For impossible action, it's successful if the task was truly impossible
370
+ result = observation_to_tool_result(
371
+ obs,
372
+ terminated=terminated,
373
+ success=environment.success,
374
+ use_images=use_images,
375
+ )
376
+ add_action_to_history(stop_action, result, ctx)
377
+ logger.info(f"Step {environment.current_step}: {obs['text']}")
378
+
379
+ # Save result if the task is terminated
380
+ save_result_if_terminated(environment, terminated, ctx)
381
+
382
+ return result
383
+ except Exception as e:
384
+ return CallToolResult(
385
+ content=[TextContent(text=str(e), type="text")], isError=True
386
+ )
387
+
388
+
389
+ @app.tool(
390
+ name="start_plancraft_task",
391
+ description="Start a new Plancraft environment (default use_images=False)",
392
+ )
393
+ def start_plancraft(use_images: bool, ctx: Context) -> CallToolResult:
394
+ """Tool that uses initialized resources"""
395
+ # Check if there's an existing environment and if it wasn't terminated yet
396
+ current_env = ctx.request_context.lifespan_context.env
397
+ if current_env and not current_env.stopped:
398
+ # Save the current environment state in results before discarding
399
+ logger.info("Discarding existing incomplete task")
400
+ # Reset history when starting a new task
401
+ ctx.request_context.lifespan_context.history = []
402
+
403
+ ctx.request_context.lifespan_context.use_images = use_images
404
+
405
+ random_idx = random.randint(
406
+ 0, len(ctx.request_context.lifespan_context.examples) - 1
407
+ )
408
+ example: PlancraftExample = ctx.request_context.lifespan_context.examples[
409
+ random_idx
410
+ ]
411
+ # initialize the environment
412
+ env = PlancraftMCPWrapper(
413
+ example=example,
414
+ max_steps=ctx.request_context.lifespan_context.max_steps,
415
+ resolution=ctx.request_context.lifespan_context.resolution,
416
+ use_text_inventory=ctx.request_context.lifespan_context.use_text_inventory,
417
+ )
418
+ logger.info(f"Environment initialized with example {example.id}")
419
+ ctx.request_context.lifespan_context.env = env
420
+
421
+ obs, _ = env.step()
422
+ logger.info(f"Step {env.current_step}: {obs['text']}")
423
+
424
+ result = observation_to_tool_result(obs, add_instructions=True)
425
+ return result
426
+
427
+
428
+ if __name__ == "__main__":
429
+ app.run()
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plancraft
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Plancraft: an evaluation dataset for planning with LLM agents
5
5
  License: MIT License
6
6
 
@@ -2,26 +2,25 @@ plancraft/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  plancraft/config.py,sha256=oyn8I_k0Slh-Nyg2javomFertZ5ZHiY_ndAVqfJYQvQ,4010
3
3
  plancraft/evaluator.py,sha256=UQujiltf88rCnbNwoglM5tJe5gW9XASew-jLaEbtJZo,15525
4
4
  plancraft/generate_dataset.py,sha256=DlrU-PmvWqSNJD1g1-8Lpb8n3N-Ogw3rje1nrRzjGKs,2382
5
+ plancraft/mcp.py,sha256=0DHYAtdgl-CD_oqKpWnTF7L1RdegPK1wU8Naq6dtVIU,15165
5
6
  plancraft/simple.py,sha256=7D_SVT2sbXLnHA98P2E_nxV5VPBKbNpij4hB2ArURxA,7329
7
+ plancraft/simple_evaluator.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
8
  plancraft/utils.py,sha256=hCE1oQ-77Me39Vo-sCL7iZPdO-WWYZnBjP41lZWRi20,6339
7
- plancraft/data/test.curriculum.json,sha256=_mqIqwcmRCcgVA5fOKK27-y3sJdsW86nEqa6ATdyTeA,875349
8
9
  plancraft/data/test.json,sha256=4jWfYMAVuZCFmGB4iZJAjlh9_8jXECdaGp8xn7_tAM4,1317131
9
10
  plancraft/data/test.small.easy.json,sha256=5NZEJ2PqIgmHQecJOIVQyM1D6GFKyJq7GVmgRudaqQk,189304
10
11
  plancraft/data/test.small.json,sha256=eULAG1rdolRMXPrecV-7YoDIheKGyIT5MVpWdISV0wg,270089
11
- plancraft/data/train.curriculum.json,sha256=eOfGusDtGG1BBvTJdzjUdhTHVAfFro_vGyYgKzZuxvE,1787758
12
12
  plancraft/data/train.json,sha256=asZIFnkBdgupPKRXacM4J0Ngt21B2BrMT6oPgFA96HI,2697710
13
- plancraft/data/val.curriculum.json,sha256=fso4S2g0kqMk7Ehz6ObQmJHk4wq0gAD2YQm0R8Nb95E,866889
14
13
  plancraft/data/val.json,sha256=IToAiaqUNQi_xhX1bzmInuskLaT7C2ryQjP-CZkzL24,1304403
15
14
  plancraft/data/val.small.easy.json,sha256=9zEmqepjXG2NIp88xnFqOCkwsUsku3HEwHoQGxgTr6U,190252
16
15
  plancraft/data/val.small.json,sha256=76E9EFaljDQyAokg97e-IblvcOe6KbrdKkXvRxhhkgo,237653
17
16
  plancraft/environment/__init__.py,sha256=XFsFny4lH195AwAmL-WeCaF9ZCMgc7IgXIwhQ8FTdgE,505
18
17
  plancraft/environment/actions.py,sha256=Pub21caxM5iZ9IaX-ny1-xxr_peJIwwV_QAx3BVSry0,11551
19
- plancraft/environment/env.py,sha256=A4532st7JFBYBF_Nh0CEEi3ZTLJAeaB3t9PAIVSemj0,16390
18
+ plancraft/environment/env.py,sha256=B7VpIMcQKITyDmkHgBoR4KbmxmM1b4A6Y-1_b90EMXo,16428
20
19
  plancraft/environment/items.py,sha256=Z9rhSyVDEoHF1pxRvhyiT94tyQJaWHi3wUHVcamz82o,221
21
20
  plancraft/environment/planner.py,sha256=uIOJjIoyT_4pxeWeTKb8BkLJyKZG0-AMoEOkZs6Ua9A,19340
22
21
  plancraft/environment/prompts.py,sha256=NU9YHAz3id-IgaukQvEi5uLlpEstpE5_Hccvvq1At2Y,6950
23
22
  plancraft/environment/recipes.py,sha256=0vwzOU86eZmGN2EpZVSIvzxpx0AOBWNPxTtAOFBN2A0,19570
24
- plancraft/environment/sampler.py,sha256=F9_lI6cyg79HkeSLJVyvh9yZkT-B9PlJWyqaoAfAhwY,7502
23
+ plancraft/environment/sampler.py,sha256=BworSMWQ-TLbV9068tkNOdo4ZLP-UDox6Laeb4Weu9k,7723
25
24
  plancraft/environment/search.py,sha256=z31eEwQBY7WJaYVBEEwulFS8P3h1Nwo1Th9BaCTxk5M,2085
26
25
  plancraft/environment/assets/constants.json,sha256=kyOIOh82CTTMMGEIS60k5k6M-6fkEmYDoGAnvi3Zx5k,1379016
27
26
  plancraft/environment/assets/minecraft_font.ttf,sha256=AzoK9cgggXwjFPHtIO7uz-YaDrminl3nvB-VsaTvTAk,60992
@@ -1924,7 +1923,7 @@ plancraft/models/generators.py,sha256=7COMLjjx_HbTWJqINNLqqExQv7gLikfLTViacAdSt5
1924
1923
  plancraft/models/oracle.py,sha256=f-0KWlBuHy6wcxmDsxM3MQ_QwfBstzfbA26mlk1MgLA,1657
1925
1924
  plancraft/models/utils.py,sha256=xgkP5jqCeFfkKe3Xd4ZYfTqiEJ-dA-qgFAC-J35ub3E,4029
1926
1925
  plancraft/train/dataset.py,sha256=oFqEd4LG9oEQ-71teh0Wf7-jJbtybT2ZibfM2bBdBkM,5474
1927
- plancraft-0.4.2.dist-info/METADATA,sha256=OMyTZhI_6V_7geLfA1i7g3sJTB9dh6LKZB76Ys3aqLY,12391
1928
- plancraft-0.4.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1929
- plancraft-0.4.2.dist-info/licenses/LICENSE,sha256=YGR8ehDB4t-T-lOQKMfKNR-2zsOU7E3E5NA8t25HKE0,1070
1930
- plancraft-0.4.2.dist-info/RECORD,,
1926
+ plancraft-0.4.3.dist-info/METADATA,sha256=wyA9iis_caO9cCKd5h5guPpuhRXi0pTkmR0dBXsbaHc,12391
1927
+ plancraft-0.4.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
1928
+ plancraft-0.4.3.dist-info/licenses/LICENSE,sha256=YGR8ehDB4t-T-lOQKMfKNR-2zsOU7E3E5NA8t25HKE0,1070
1929
+ plancraft-0.4.3.dist-info/RECORD,,