foodforthought-cli 0.2.8__py3-none-any.whl → 0.3.1__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.
Files changed (116) hide show
  1. ate/__init__.py +6 -0
  2. ate/__main__.py +16 -0
  3. ate/auth/__init__.py +1 -0
  4. ate/auth/device_flow.py +141 -0
  5. ate/auth/token_store.py +96 -0
  6. ate/behaviors/__init__.py +12 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/cli.py +855 -4551
  9. ate/client.py +90 -0
  10. ate/commands/__init__.py +168 -0
  11. ate/commands/auth.py +389 -0
  12. ate/commands/bridge.py +448 -0
  13. ate/commands/data.py +185 -0
  14. ate/commands/deps.py +111 -0
  15. ate/commands/generate.py +384 -0
  16. ate/commands/memory.py +907 -0
  17. ate/commands/parts.py +166 -0
  18. ate/commands/primitive.py +399 -0
  19. ate/commands/protocol.py +288 -0
  20. ate/commands/recording.py +524 -0
  21. ate/commands/repo.py +154 -0
  22. ate/commands/simulation.py +291 -0
  23. ate/commands/skill.py +303 -0
  24. ate/commands/skills.py +487 -0
  25. ate/commands/team.py +147 -0
  26. ate/commands/workflow.py +271 -0
  27. ate/detection/__init__.py +38 -0
  28. ate/detection/base.py +142 -0
  29. ate/detection/color_detector.py +402 -0
  30. ate/detection/trash_detector.py +322 -0
  31. ate/drivers/__init__.py +18 -6
  32. ate/drivers/ble_transport.py +405 -0
  33. ate/drivers/mechdog.py +360 -24
  34. ate/drivers/wifi_camera.py +477 -0
  35. ate/interfaces/__init__.py +16 -0
  36. ate/interfaces/base.py +2 -0
  37. ate/interfaces/sensors.py +247 -0
  38. ate/llm_proxy.py +239 -0
  39. ate/memory/__init__.py +35 -0
  40. ate/memory/cloud.py +244 -0
  41. ate/memory/context.py +269 -0
  42. ate/memory/embeddings.py +184 -0
  43. ate/memory/export.py +26 -0
  44. ate/memory/merge.py +146 -0
  45. ate/memory/migrate/__init__.py +34 -0
  46. ate/memory/migrate/base.py +89 -0
  47. ate/memory/migrate/pipeline.py +189 -0
  48. ate/memory/migrate/sources/__init__.py +13 -0
  49. ate/memory/migrate/sources/chroma.py +170 -0
  50. ate/memory/migrate/sources/pinecone.py +120 -0
  51. ate/memory/migrate/sources/qdrant.py +110 -0
  52. ate/memory/migrate/sources/weaviate.py +160 -0
  53. ate/memory/reranker.py +353 -0
  54. ate/memory/search.py +26 -0
  55. ate/memory/store.py +548 -0
  56. ate/recording/__init__.py +42 -3
  57. ate/recording/session.py +12 -2
  58. ate/recording/visual.py +416 -0
  59. ate/robot/__init__.py +142 -0
  60. ate/robot/agentic_servo.py +856 -0
  61. ate/robot/behaviors.py +493 -0
  62. ate/robot/ble_capture.py +1000 -0
  63. ate/robot/ble_enumerate.py +506 -0
  64. ate/robot/calibration.py +88 -3
  65. ate/robot/calibration_state.py +388 -0
  66. ate/robot/commands.py +143 -11
  67. ate/robot/direction_calibration.py +554 -0
  68. ate/robot/discovery.py +104 -2
  69. ate/robot/llm_system_id.py +654 -0
  70. ate/robot/locomotion_calibration.py +508 -0
  71. ate/robot/marker_generator.py +611 -0
  72. ate/robot/perception.py +502 -0
  73. ate/robot/primitives.py +614 -0
  74. ate/robot/profiles.py +6 -0
  75. ate/robot/registry.py +5 -2
  76. ate/robot/servo_mapper.py +1153 -0
  77. ate/robot/skill_upload.py +285 -3
  78. ate/robot/target_calibration.py +500 -0
  79. ate/robot/teach.py +515 -0
  80. ate/robot/types.py +242 -0
  81. ate/robot/visual_labeler.py +9 -0
  82. ate/robot/visual_servo_loop.py +494 -0
  83. ate/robot/visual_servoing.py +570 -0
  84. ate/robot/visual_system_id.py +906 -0
  85. ate/transports/__init__.py +121 -0
  86. ate/transports/base.py +394 -0
  87. ate/transports/ble.py +405 -0
  88. ate/transports/hybrid.py +444 -0
  89. ate/transports/serial.py +345 -0
  90. ate/urdf/__init__.py +30 -0
  91. ate/urdf/capture.py +582 -0
  92. ate/urdf/cloud.py +491 -0
  93. ate/urdf/collision.py +271 -0
  94. ate/urdf/commands.py +708 -0
  95. ate/urdf/depth.py +360 -0
  96. ate/urdf/inertial.py +312 -0
  97. ate/urdf/kinematics.py +330 -0
  98. ate/urdf/lifting.py +415 -0
  99. ate/urdf/meshing.py +300 -0
  100. ate/urdf/models/__init__.py +110 -0
  101. ate/urdf/models/depth_anything.py +253 -0
  102. ate/urdf/models/sam2.py +324 -0
  103. ate/urdf/motion_analysis.py +396 -0
  104. ate/urdf/pipeline.py +468 -0
  105. ate/urdf/scale.py +256 -0
  106. ate/urdf/scan_session.py +411 -0
  107. ate/urdf/segmentation.py +299 -0
  108. ate/urdf/synthesis.py +319 -0
  109. ate/urdf/topology.py +336 -0
  110. ate/urdf/validation.py +371 -0
  111. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/METADATA +1 -1
  112. foodforthought_cli-0.3.1.dist-info/RECORD +166 -0
  113. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/WHEEL +1 -1
  114. foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
  115. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/entry_points.txt +0 -0
  116. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.1.dist-info}/top_level.txt +0 -0
ate/commands/bridge.py ADDED
@@ -0,0 +1,448 @@
1
+ """
2
+ Bridge commands for FoodforThought CLI.
3
+
4
+ Commands:
5
+ - ate bridge connect - Connect to robot interactively
6
+ - ate bridge send - Send single command
7
+ - ate bridge record - Record session for primitive creation
8
+ - ate bridge replay - Replay a recorded session
9
+ - ate bridge serve - Start WebSocket server for Artifex integration
10
+ """
11
+
12
+ import json
13
+ import sys
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+
19
+ def bridge_connect(client, port: str, transport: str,
20
+ baud_rate: int, protocol_id: Optional[str]) -> None:
21
+ """Connect to a robot via serial or BLE and start interactive session."""
22
+ print(f"\n{'=' * 60}")
23
+ print("FoodForThought Bridge - Robot Communication Interface")
24
+ print(f"{'=' * 60}\n")
25
+
26
+ if transport == "serial":
27
+ try:
28
+ import serial
29
+ except ImportError:
30
+ print("Error: pyserial is required. Install with: pip install pyserial", file=sys.stderr)
31
+ sys.exit(1)
32
+
33
+ print(f"Connecting to {port} at {baud_rate} baud...")
34
+ try:
35
+ ser = serial.Serial(port, baud_rate, timeout=1)
36
+ print(f"✓ Connected to {port}")
37
+ print(f"\nInteractive mode. Type commands to send to robot.")
38
+ print("Special commands:")
39
+ print(" .quit - Exit bridge")
40
+ print(" .record - Start recording for primitive creation")
41
+ print(" .stop - Stop recording")
42
+ print(" .save - Save recorded session")
43
+ print(" .protocol - Show loaded protocol info")
44
+ print("-" * 60 + "\n")
45
+
46
+ recording = False
47
+ recorded_commands = []
48
+
49
+ while True:
50
+ try:
51
+ cmd = input("bridge> ").strip()
52
+ if not cmd:
53
+ continue
54
+
55
+ if cmd == ".quit":
56
+ break
57
+ elif cmd == ".record":
58
+ recording = True
59
+ recorded_commands = []
60
+ print("Recording started...")
61
+ continue
62
+ elif cmd == ".stop":
63
+ recording = False
64
+ print(f"Recording stopped. {len(recorded_commands)} commands recorded.")
65
+ continue
66
+ elif cmd == ".save":
67
+ if recorded_commands:
68
+ filename = f"session_{int(time.time())}.json"
69
+ with open(filename, "w") as f:
70
+ json.dump({
71
+ "port": port,
72
+ "baud_rate": baud_rate,
73
+ "commands": recorded_commands
74
+ }, f, indent=2)
75
+ print(f"Session saved to {filename}")
76
+ else:
77
+ print("No commands recorded yet.")
78
+ continue
79
+ elif cmd == ".protocol":
80
+ if protocol_id:
81
+ from . import protocol
82
+ protocol.protocol_get(client, protocol_id)
83
+ else:
84
+ print("No protocol loaded. Use --protocol flag to specify.")
85
+ continue
86
+
87
+ # Send command to robot
88
+ ser.write((cmd + "\n").encode())
89
+ if recording:
90
+ recorded_commands.append({
91
+ "command": cmd,
92
+ "timestamp": time.time()
93
+ })
94
+
95
+ # Read response
96
+ time.sleep(0.1)
97
+ response = ser.read_all().decode("utf-8", errors="replace").strip()
98
+ if response:
99
+ print(f"< {response}")
100
+ if recording:
101
+ recorded_commands[-1]["response"] = response
102
+
103
+ except KeyboardInterrupt:
104
+ break
105
+
106
+ ser.close()
107
+ print("\n✓ Disconnected")
108
+
109
+ except Exception as e:
110
+ print(f"Error connecting to {port}: {e}", file=sys.stderr)
111
+ sys.exit(1)
112
+
113
+ elif transport == "ble":
114
+ print("BLE bridge requires async operation. Starting BLE session...")
115
+ _bridge_ble_connect(port)
116
+
117
+
118
+ def _bridge_ble_connect(address: str) -> None:
119
+ """Connect to robot via BLE (requires bleak)."""
120
+ try:
121
+ import asyncio
122
+ from bleak import BleakClient
123
+ except ImportError:
124
+ print("Error: bleak is required for BLE. Install with: pip install bleak", file=sys.stderr)
125
+ sys.exit(1)
126
+
127
+ async def ble_session():
128
+ print(f"Connecting to BLE device {address}...")
129
+ try:
130
+ async with BleakClient(address) as client:
131
+ print(f"✓ Connected to {address}")
132
+
133
+ print("\nAvailable services:")
134
+ for service in client.services:
135
+ print(f" {service.uuid}: {service.description or 'Unknown'}")
136
+ for char in service.characteristics:
137
+ props = ", ".join(char.properties)
138
+ print(f" └─ {char.uuid} [{props}]")
139
+
140
+ print("\nInteractive mode. Commands:")
141
+ print(" read <uuid> - Read characteristic")
142
+ print(" write <uuid> <hex> - Write to characteristic")
143
+ print(" notify <uuid> - Subscribe to notifications")
144
+ print(" .quit - Exit")
145
+ print("-" * 60 + "\n")
146
+
147
+ while True:
148
+ try:
149
+ cmd = input("ble> ").strip()
150
+ if not cmd:
151
+ continue
152
+
153
+ if cmd == ".quit":
154
+ break
155
+
156
+ parts = cmd.split()
157
+ if parts[0] == "read" and len(parts) >= 2:
158
+ uuid = parts[1]
159
+ try:
160
+ data = await client.read_gatt_char(uuid)
161
+ print(f"< {data.hex()} ({data})")
162
+ except Exception as e:
163
+ print(f"Error reading: {e}")
164
+
165
+ elif parts[0] == "write" and len(parts) >= 3:
166
+ uuid = parts[1]
167
+ hex_data = parts[2]
168
+ try:
169
+ data = bytes.fromhex(hex_data)
170
+ await client.write_gatt_char(uuid, data)
171
+ print(f"✓ Written {hex_data}")
172
+ except Exception as e:
173
+ print(f"Error writing: {e}")
174
+
175
+ elif parts[0] == "notify" and len(parts) >= 2:
176
+ uuid = parts[1]
177
+
178
+ def callback(sender, data):
179
+ print(f"[{sender}] {data.hex()}")
180
+
181
+ try:
182
+ await client.start_notify(uuid, callback)
183
+ print(f"✓ Subscribed to {uuid}")
184
+ except Exception as e:
185
+ print(f"Error subscribing: {e}")
186
+
187
+ else:
188
+ print("Unknown command. Use read/write/notify or .quit")
189
+
190
+ except KeyboardInterrupt:
191
+ break
192
+
193
+ print("\n✓ Disconnected")
194
+
195
+ except Exception as e:
196
+ print(f"Error connecting to BLE device: {e}", file=sys.stderr)
197
+ sys.exit(1)
198
+
199
+ asyncio.run(ble_session())
200
+
201
+
202
+ def bridge_send(client, port: str, command: str, transport: str,
203
+ baud_rate: int, wait: float) -> None:
204
+ """Send a single command to robot and print response."""
205
+ if transport == "serial":
206
+ try:
207
+ import serial
208
+ except ImportError:
209
+ print("Error: pyserial is required. Install with: pip install pyserial", file=sys.stderr)
210
+ sys.exit(1)
211
+
212
+ try:
213
+ ser = serial.Serial(port, baud_rate, timeout=1)
214
+ ser.write((command + "\n").encode())
215
+ time.sleep(wait)
216
+ response = ser.read_all().decode("utf-8", errors="replace").strip()
217
+ if response:
218
+ print(response)
219
+ ser.close()
220
+ except Exception as e:
221
+ print(f"Error: {e}", file=sys.stderr)
222
+ sys.exit(1)
223
+ else:
224
+ print("BLE send not yet implemented. Use bridge connect for BLE.", file=sys.stderr)
225
+
226
+
227
+ def bridge_record(client, port: str, output: str, transport: str,
228
+ baud_rate: int, primitive_name: Optional[str]) -> None:
229
+ """Record a command session for creating a primitive skill."""
230
+ print(f"\n{'=' * 60}")
231
+ print("FoodForThought Bridge - Recording Mode")
232
+ print(f"{'=' * 60}\n")
233
+
234
+ if transport != "serial":
235
+ print("Recording currently only supports serial connections", file=sys.stderr)
236
+ sys.exit(1)
237
+
238
+ try:
239
+ import serial
240
+ except ImportError:
241
+ print("Error: pyserial is required. Install with: pip install pyserial", file=sys.stderr)
242
+ sys.exit(1)
243
+
244
+ print(f"Connecting to {port} at {baud_rate} baud...")
245
+ try:
246
+ ser = serial.Serial(port, baud_rate, timeout=1)
247
+ print(f"✓ Connected and recording to {output}")
248
+ print(f"\nType commands to send. Ctrl+C to stop and save.\n")
249
+
250
+ recorded = {
251
+ "name": primitive_name or f"recorded_skill_{int(time.time())}",
252
+ "port": port,
253
+ "baud_rate": baud_rate,
254
+ "start_time": time.time(),
255
+ "commands": []
256
+ }
257
+
258
+ while True:
259
+ try:
260
+ cmd = input(f"[REC] bridge> ").strip()
261
+ if not cmd:
262
+ continue
263
+
264
+ timestamp = time.time()
265
+ ser.write((cmd + "\n").encode())
266
+ time.sleep(0.1)
267
+ response = ser.read_all().decode("utf-8", errors="replace").strip()
268
+
269
+ entry = {
270
+ "command": cmd,
271
+ "timestamp": timestamp - recorded["start_time"],
272
+ "response": response
273
+ }
274
+ recorded["commands"].append(entry)
275
+
276
+ if response:
277
+ print(f"< {response}")
278
+
279
+ except KeyboardInterrupt:
280
+ break
281
+
282
+ ser.close()
283
+ recorded["end_time"] = time.time()
284
+ recorded["duration"] = recorded["end_time"] - recorded["start_time"]
285
+
286
+ with open(output, "w") as f:
287
+ json.dump(recorded, f, indent=2)
288
+
289
+ print(f"\n✓ Recorded {len(recorded['commands'])} commands")
290
+ print(f"✓ Saved to {output}")
291
+ print(f"\nTo create a primitive from this recording:")
292
+ print(f" ate primitive init --from-recording {output}")
293
+
294
+ except Exception as e:
295
+ print(f"Error: {e}", file=sys.stderr)
296
+ sys.exit(1)
297
+
298
+
299
+ def bridge_replay(client, recording_file: str, port: str, transport: str,
300
+ baud_rate: int, speed: float) -> None:
301
+ """Replay a recorded session."""
302
+ if not Path(recording_file).exists():
303
+ print(f"Error: Recording file not found: {recording_file}", file=sys.stderr)
304
+ sys.exit(1)
305
+
306
+ with open(recording_file) as f:
307
+ recording = json.load(f)
308
+
309
+ if transport != "serial":
310
+ print("Replay currently only supports serial connections", file=sys.stderr)
311
+ sys.exit(1)
312
+
313
+ try:
314
+ import serial
315
+ except ImportError:
316
+ print("Error: pyserial is required. Install with: pip install pyserial", file=sys.stderr)
317
+ sys.exit(1)
318
+
319
+ print(f"\n{'=' * 60}")
320
+ print(f"Replaying: {recording.get('name', recording_file)}")
321
+ print(f"Commands: {len(recording.get('commands', []))}")
322
+ print(f"Speed: {speed}x")
323
+ print(f"{'=' * 60}\n")
324
+
325
+ try:
326
+ ser = serial.Serial(port, baud_rate, timeout=1)
327
+ commands = recording.get("commands", [])
328
+
329
+ prev_timestamp = 0
330
+ for i, entry in enumerate(commands):
331
+ timestamp = entry.get("timestamp", 0)
332
+ delay = (timestamp - prev_timestamp) / speed
333
+ if delay > 0 and i > 0:
334
+ time.sleep(delay)
335
+ prev_timestamp = timestamp
336
+
337
+ cmd = entry.get("command", "")
338
+ print(f"[{i + 1}/{len(commands)}] > {cmd}")
339
+ ser.write((cmd + "\n").encode())
340
+
341
+ time.sleep(0.1)
342
+ response = ser.read_all().decode("utf-8", errors="replace").strip()
343
+ if response:
344
+ print(f" < {response}")
345
+
346
+ ser.close()
347
+ print(f"\n✓ Replay complete")
348
+
349
+ except Exception as e:
350
+ print(f"Error during replay: {e}", file=sys.stderr)
351
+ sys.exit(1)
352
+
353
+
354
+ def bridge_serve(port: int, verbose: bool) -> None:
355
+ """Start WebSocket server for Artifex Desktop integration."""
356
+ from ate.bridge_server import run_bridge_server
357
+ run_bridge_server(port=port, verbose=verbose)
358
+
359
+
360
+ def register_parser(subparsers):
361
+ """Register bridge commands with argparse."""
362
+ import argparse
363
+
364
+ bridge_parser = subparsers.add_parser("bridge", help="Interactive robot communication bridge")
365
+ bridge_subparsers = bridge_parser.add_subparsers(dest="bridge_action", help="Bridge action")
366
+
367
+ # bridge connect
368
+ bridge_connect_parser = bridge_subparsers.add_parser("connect",
369
+ help="Connect to robot interactively")
370
+ bridge_connect_parser.add_argument("port", help="Serial port or BLE address")
371
+ bridge_connect_parser.add_argument("-t", "--transport", default="serial",
372
+ choices=["serial", "ble"],
373
+ help="Transport type (default: serial)")
374
+ bridge_connect_parser.add_argument("-b", "--baud", type=int, default=115200,
375
+ help="Baud rate for serial (default: 115200)")
376
+ bridge_connect_parser.add_argument("-p", "--protocol", help="Protocol ID for command hints")
377
+
378
+ # bridge send
379
+ bridge_send_parser = bridge_subparsers.add_parser("send", help="Send single command")
380
+ bridge_send_parser.add_argument("port", help="Serial port or BLE address")
381
+ bridge_send_parser.add_argument("command", help="Command to send")
382
+ bridge_send_parser.add_argument("-t", "--transport", default="serial",
383
+ choices=["serial", "ble"],
384
+ help="Transport type (default: serial)")
385
+ bridge_send_parser.add_argument("-b", "--baud", type=int, default=115200,
386
+ help="Baud rate for serial (default: 115200)")
387
+ bridge_send_parser.add_argument("-w", "--wait", type=float, default=0.5,
388
+ help="Wait time for response in seconds (default: 0.5)")
389
+
390
+ # bridge record
391
+ bridge_record_parser = bridge_subparsers.add_parser("record",
392
+ help="Record session for primitive creation")
393
+ bridge_record_parser.add_argument("port", help="Serial port or BLE address")
394
+ bridge_record_parser.add_argument("-o", "--output", default="./recording.json",
395
+ help="Output file (default: ./recording.json)")
396
+ bridge_record_parser.add_argument("-t", "--transport", default="serial",
397
+ choices=["serial", "ble"],
398
+ help="Transport type (default: serial)")
399
+ bridge_record_parser.add_argument("-b", "--baud", type=int, default=115200,
400
+ help="Baud rate for serial (default: 115200)")
401
+ bridge_record_parser.add_argument("-n", "--name", help="Primitive skill name")
402
+
403
+ # bridge replay
404
+ bridge_replay_parser = bridge_subparsers.add_parser("replay", help="Replay a recorded session")
405
+ bridge_replay_parser.add_argument("recording", help="Path to recording file")
406
+ bridge_replay_parser.add_argument("port", help="Serial port or BLE address")
407
+ bridge_replay_parser.add_argument("-t", "--transport", default="serial",
408
+ choices=["serial", "ble"],
409
+ help="Transport type (default: serial)")
410
+ bridge_replay_parser.add_argument("-b", "--baud", type=int, default=115200,
411
+ help="Baud rate for serial (default: 115200)")
412
+ bridge_replay_parser.add_argument("-s", "--speed", type=float, default=1.0,
413
+ help="Playback speed multiplier (default: 1.0)")
414
+
415
+ # bridge serve
416
+ bridge_serve_parser = bridge_subparsers.add_parser("serve",
417
+ help="Start WebSocket server for Artifex Desktop integration",
418
+ description="""Start the ATE Bridge Server for Artifex Desktop integration.
419
+
420
+ This server enables sim-to-real transfer by bridging Artifex Desktop to physical robot hardware.
421
+
422
+ WORKFLOW:
423
+ 1. Start this server: ate bridge serve -v
424
+ 2. Connect your robot via USB serial
425
+ 3. In Artifex Desktop, use the Hardware panel or AI tools to connect
426
+ 4. Control your robot directly from the Artifex interface""",
427
+ formatter_class=argparse.RawDescriptionHelpFormatter)
428
+ bridge_serve_parser.add_argument("-p", "--port", type=int, default=8765,
429
+ help="WebSocket port (default: 8765)")
430
+ bridge_serve_parser.add_argument("-v", "--verbose", action="store_true",
431
+ help="Enable verbose logging")
432
+
433
+
434
+ def handle(client, args):
435
+ """Handle bridge commands."""
436
+ if args.bridge_action == "connect":
437
+ bridge_connect(client, args.port, args.transport, args.baud, args.protocol)
438
+ elif args.bridge_action == "send":
439
+ bridge_send(client, args.port, args.command, args.transport, args.baud, args.wait)
440
+ elif args.bridge_action == "record":
441
+ bridge_record(client, args.port, args.output, args.transport, args.baud, args.name)
442
+ elif args.bridge_action == "replay":
443
+ bridge_replay(client, args.recording, args.port, args.transport, args.baud, args.speed)
444
+ elif args.bridge_action == "serve":
445
+ bridge_serve(args.port, args.verbose)
446
+ else:
447
+ print("Usage: ate bridge {connect|send|record|replay|serve}")
448
+ sys.exit(1)
ate/commands/data.py ADDED
@@ -0,0 +1,185 @@
1
+ """
2
+ Data management commands for FoodforThought CLI.
3
+
4
+ Commands:
5
+ - ate data upload - Upload dataset/sensor logs
6
+ - ate data list - List datasets
7
+ - ate data promote - Promote dataset to next stage
8
+ - ate data export - Export dataset in specified format
9
+ """
10
+
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Optional
15
+
16
+
17
+ def data_upload(client, path: str, skill: str, stage: str) -> None:
18
+ """Upload dataset/sensor logs."""
19
+ data_path = Path(path)
20
+
21
+ if not data_path.exists():
22
+ print(f"Error: Path not found: {path}", file=sys.stderr)
23
+ sys.exit(1)
24
+
25
+ print(f"Uploading data...")
26
+ print(f" Path: {path}")
27
+ print(f" Skill: {skill}")
28
+ print(f" Stage: {stage}")
29
+
30
+ # Count files
31
+ if data_path.is_dir():
32
+ files = list(data_path.rglob("*"))
33
+ file_count = len([f for f in files if f.is_file()])
34
+ total_size = sum(f.stat().st_size for f in files if f.is_file())
35
+ else:
36
+ file_count = 1
37
+ total_size = data_path.stat().st_size
38
+
39
+ print(f" Files: {file_count}")
40
+ print(f" Size: {total_size / 1024 / 1024:.1f} MB")
41
+
42
+ try:
43
+ response = client._request("POST", "/datasets/upload", json={
44
+ "skillId": skill,
45
+ "stage": stage,
46
+ "fileCount": file_count,
47
+ "totalSize": total_size,
48
+ })
49
+
50
+ dataset = response.get("dataset", {})
51
+ print(f"\n✓ Dataset uploaded!")
52
+ print(f" Dataset ID: {dataset.get('id')}")
53
+ print(f" Stage: {dataset.get('stage')}")
54
+
55
+ except Exception as e:
56
+ print(f"\n✗ Upload failed: {e}", file=sys.stderr)
57
+ sys.exit(1)
58
+
59
+
60
+ def data_list(client, skill: Optional[str], stage: Optional[str]) -> None:
61
+ """List datasets."""
62
+ print("Fetching datasets...")
63
+
64
+ params = {}
65
+ if skill:
66
+ params["skill"] = skill
67
+ if stage:
68
+ params["stage"] = stage
69
+
70
+ try:
71
+ response = client._request("GET", "/datasets", params=params)
72
+ datasets = response.get("datasets", [])
73
+
74
+ if not datasets:
75
+ print("\nNo datasets found.")
76
+ return
77
+
78
+ print(f"\n{'=' * 70}")
79
+ print(f"{'Name':<30} {'Stage':<15} {'Size':<15} {'Created':<15}")
80
+ print(f"{'=' * 70}")
81
+
82
+ for ds in datasets:
83
+ name = ds.get("name", "Unnamed")[:28]
84
+ ds_stage = ds.get("stage", "unknown")[:13]
85
+ size = f"{ds.get('size', 0) / 1024 / 1024:.1f} MB"
86
+ created = ds.get("createdAt", "")[:10]
87
+ print(f"{name:<30} {ds_stage:<15} {size:<15} {created:<15}")
88
+
89
+ except Exception as e:
90
+ print(f"\n✗ Failed to list: {e}", file=sys.stderr)
91
+ sys.exit(1)
92
+
93
+
94
+ def data_promote(client, dataset_id: str, to_stage: str) -> None:
95
+ """Promote dataset to next stage."""
96
+ print(f"Promoting dataset...")
97
+ print(f" Dataset: {dataset_id}")
98
+ print(f" New Stage: {to_stage}")
99
+
100
+ try:
101
+ client._request("PATCH", f"/datasets/{dataset_id}/promote", json={
102
+ "stage": to_stage,
103
+ })
104
+
105
+ print(f"\n✓ Dataset promoted to {to_stage}!")
106
+
107
+ except Exception as e:
108
+ print(f"\n✗ Promotion failed: {e}", file=sys.stderr)
109
+ sys.exit(1)
110
+
111
+
112
+ def data_export(client, dataset_id: str, format: str, output: str) -> None:
113
+ """Export dataset in specified format."""
114
+ print(f"Exporting dataset...")
115
+ print(f" Dataset: {dataset_id}")
116
+ print(f" Format: {format}")
117
+ print(f" Output: {output}")
118
+
119
+ try:
120
+ response = client._request("GET", f"/datasets/{dataset_id}/export",
121
+ params={"format": format})
122
+
123
+ # Save export
124
+ output_path = Path(output)
125
+ output_path.mkdir(parents=True, exist_ok=True)
126
+
127
+ export_file = output_path / f"{dataset_id}.{format}"
128
+ with open(export_file, 'w') as f:
129
+ json.dump(response, f, indent=2)
130
+
131
+ print(f"\n✓ Exported to: {export_file}")
132
+
133
+ except Exception as e:
134
+ print(f"\n✗ Export failed: {e}", file=sys.stderr)
135
+ sys.exit(1)
136
+
137
+
138
+ def register_parser(subparsers):
139
+ """Register data commands with argparse."""
140
+ data_parser = subparsers.add_parser("data", help="Dataset and telemetry management")
141
+ data_subparsers = data_parser.add_subparsers(dest="data_action", help="Data action")
142
+
143
+ # data upload
144
+ data_upload_parser = data_subparsers.add_parser("upload", help="Upload sensor data")
145
+ data_upload_parser.add_argument("path", help="Path to data directory or file")
146
+ data_upload_parser.add_argument("-s", "--skill", required=True, help="Associated skill ID")
147
+ data_upload_parser.add_argument("--stage", default="raw",
148
+ choices=["raw", "annotated", "skill-abstracted", "production"],
149
+ help="Data stage (default: raw)")
150
+
151
+ # data list
152
+ data_list_parser = data_subparsers.add_parser("list", help="List datasets")
153
+ data_list_parser.add_argument("-s", "--skill", help="Filter by skill ID")
154
+ data_list_parser.add_argument("--stage", help="Filter by stage")
155
+
156
+ # data promote
157
+ data_promote_parser = data_subparsers.add_parser("promote", help="Promote dataset stage")
158
+ data_promote_parser.add_argument("dataset_id", help="Dataset ID")
159
+ data_promote_parser.add_argument("--to", required=True, dest="to_stage",
160
+ choices=["annotated", "skill-abstracted", "production"],
161
+ help="Target stage")
162
+
163
+ # data export
164
+ data_export_parser = data_subparsers.add_parser("export", help="Export dataset")
165
+ data_export_parser.add_argument("dataset_id", help="Dataset ID")
166
+ data_export_parser.add_argument("-f", "--format", default="rlds",
167
+ choices=["json", "rlds", "lerobot", "hdf5"],
168
+ help="Export format (default: rlds)")
169
+ data_export_parser.add_argument("-o", "--output", default="./export",
170
+ help="Output directory")
171
+
172
+
173
+ def handle(client, args):
174
+ """Handle data commands."""
175
+ if args.data_action == "upload":
176
+ data_upload(client, args.path, args.skill, args.stage)
177
+ elif args.data_action == "list":
178
+ data_list(client, args.skill, args.stage)
179
+ elif args.data_action == "promote":
180
+ data_promote(client, args.dataset_id, args.to_stage)
181
+ elif args.data_action == "export":
182
+ data_export(client, args.dataset_id, args.format, args.output)
183
+ else:
184
+ print("Usage: ate data {upload|list|promote|export}")
185
+ sys.exit(1)