foodforthought-cli 0.2.8__py3-none-any.whl → 0.3.0__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 +399 -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.0.dist-info}/METADATA +1 -1
  112. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  113. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.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.0.dist-info}/entry_points.txt +0 -0
  116. {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,524 @@
1
+ """
2
+ Recording commands for FoodforThought CLI.
3
+
4
+ Commands:
5
+ - ate record start - Start recording telemetry
6
+ - ate record stop - Stop recording and upload
7
+ - ate record status - Get current recording status
8
+ - ate record demo - Record a timed demonstration
9
+ - ate record list - List telemetry recordings
10
+ - ate record local - Record from a connected robot
11
+ """
12
+
13
+ import json
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Optional, List
18
+
19
+ CONFIG_DIR = Path.home() / ".ate"
20
+
21
+
22
+ def record_start(client, robot_id: str, skill_id: str, task_description: Optional[str] = None) -> None:
23
+ """Start recording telemetry from a robot."""
24
+ import uuid
25
+
26
+ # Store recording state in a file
27
+ recording_file = CONFIG_DIR / "active_recording.json"
28
+ CONFIG_DIR.mkdir(exist_ok=True)
29
+
30
+ if recording_file.exists():
31
+ print("Error: Recording already in progress. Run 'ate record stop' first.", file=sys.stderr)
32
+ sys.exit(1)
33
+
34
+ recording_id = str(uuid.uuid4())
35
+ recording_state = {
36
+ "id": recording_id,
37
+ "robot_id": robot_id,
38
+ "skill_id": skill_id,
39
+ "task_description": task_description or "",
40
+ "start_time": time.time(),
41
+ "frames": [],
42
+ }
43
+
44
+ with open(recording_file, "w") as f:
45
+ json.dump(recording_state, f, indent=2)
46
+
47
+ print(f"Recording started!")
48
+ print(f" Recording ID: {recording_id}")
49
+ print(f" Robot: {robot_id}")
50
+ print(f" Skill: {skill_id}")
51
+ if task_description:
52
+ print(f" Task: {task_description}")
53
+ print(f"\nRun 'ate record stop' when finished.")
54
+
55
+
56
+ def record_stop(client, success: bool = True, notes: Optional[str] = None,
57
+ upload: bool = True, create_labeling_task: bool = False) -> None:
58
+ """Stop recording and optionally upload to FoodforThought."""
59
+ from datetime import datetime
60
+
61
+ recording_file = CONFIG_DIR / "active_recording.json"
62
+
63
+ if not recording_file.exists():
64
+ print("Error: No active recording. Start one with 'ate record start'.", file=sys.stderr)
65
+ sys.exit(1)
66
+
67
+ with open(recording_file, "r") as f:
68
+ recording_state = json.load(f)
69
+
70
+ # Calculate duration
71
+ end_time = time.time()
72
+ duration = end_time - recording_state["start_time"]
73
+ frame_count = len(recording_state.get("frames", []))
74
+
75
+ print(f"Recording stopped!")
76
+ print(f" Recording ID: {recording_state['id']}")
77
+ print(f" Duration: {duration:.1f}s")
78
+ print(f" Frames: {frame_count}")
79
+ print(f" Success: {'Yes' if success else 'No'}")
80
+
81
+ if upload:
82
+ print(f"\nUploading to FoodforThought...")
83
+
84
+ try:
85
+ recording_data = {
86
+ "recording": {
87
+ "id": recording_state["id"],
88
+ "robotId": recording_state["robot_id"],
89
+ "skillId": recording_state["skill_id"],
90
+ "source": "hardware",
91
+ "startTime": datetime.fromtimestamp(recording_state["start_time"]).isoformat(),
92
+ "endTime": datetime.fromtimestamp(end_time).isoformat(),
93
+ "success": success,
94
+ "metadata": {
95
+ "duration": duration,
96
+ "frameRate": frame_count / duration if duration > 0 else 0,
97
+ "totalFrames": frame_count,
98
+ "tags": ["edge_recording", "cli"],
99
+ "notes": notes,
100
+ },
101
+ "frames": recording_state.get("frames", []),
102
+ "events": [],
103
+ },
104
+ }
105
+
106
+ if create_labeling_task:
107
+ recording_data["createLabelingTask"] = True
108
+
109
+ response = client._request("POST", "/telemetry/ingest", json=recording_data)
110
+
111
+ artifact_id = response.get("data", {}).get("artifactId", "")
112
+ print(f"\n✓ Uploaded successfully!")
113
+ print(f" Artifact ID: {artifact_id}")
114
+ print(f" View at: https://foodforthought.kindly.fyi/artifacts/{artifact_id}")
115
+
116
+ if create_labeling_task:
117
+ task_id = response.get("data", {}).get("taskId", "")
118
+ if task_id:
119
+ print(f" Labeling Task: https://foodforthought.kindly.fyi/labeling/{task_id}")
120
+
121
+ except Exception as e:
122
+ print(f"\n✗ Upload failed: {e}", file=sys.stderr)
123
+ print("Recording saved locally. You can upload later.", file=sys.stderr)
124
+
125
+ if notes:
126
+ print(f"\nNotes: {notes}")
127
+
128
+ # Remove recording state file
129
+ recording_file.unlink()
130
+
131
+
132
+ def record_status(client) -> None:
133
+ """Get current recording status."""
134
+ recording_file = CONFIG_DIR / "active_recording.json"
135
+
136
+ if not recording_file.exists():
137
+ print("No active recording session.")
138
+ return
139
+
140
+ with open(recording_file, "r") as f:
141
+ recording_state = json.load(f)
142
+
143
+ elapsed = time.time() - recording_state["start_time"]
144
+ frame_count = len(recording_state.get("frames", []))
145
+
146
+ print(f"Recording in progress")
147
+ print(f" Recording ID: {recording_state['id']}")
148
+ print(f" Robot: {recording_state['robot_id']}")
149
+ print(f" Skill: {recording_state['skill_id']}")
150
+ print(f" Elapsed: {elapsed:.1f}s")
151
+ print(f" Frames: {frame_count}")
152
+ if recording_state.get("task_description"):
153
+ print(f" Task: {recording_state['task_description']}")
154
+
155
+
156
+ def record_demo(client, robot_id: str, skill_id: str, task_description: str,
157
+ duration_seconds: float = 30.0, create_labeling_task: bool = True) -> None:
158
+ """Record a timed demonstration."""
159
+ import uuid
160
+ from datetime import datetime
161
+
162
+ recording_id = str(uuid.uuid4())
163
+ print(f"Recording demonstration...")
164
+ print(f" Recording ID: {recording_id}")
165
+ print(f" Robot: {robot_id}")
166
+ print(f" Skill: {skill_id}")
167
+ print(f" Task: {task_description}")
168
+ print(f" Duration: {duration_seconds}s")
169
+ print()
170
+
171
+ start_time = time.time()
172
+
173
+ # Show a countdown/progress indicator
174
+ elapsed = 0
175
+ while elapsed < duration_seconds:
176
+ remaining = duration_seconds - elapsed
177
+ print(f"\rRecording... {remaining:.0f}s remaining", end="", flush=True)
178
+ time.sleep(min(1.0, remaining))
179
+ elapsed = time.time() - start_time
180
+
181
+ end_time = time.time()
182
+ actual_duration = end_time - start_time
183
+ print(f"\rRecording complete!{' ' * 20}")
184
+
185
+ print(f"\nUploading to FoodforThought...")
186
+
187
+ try:
188
+ recording_data = {
189
+ "recording": {
190
+ "id": recording_id,
191
+ "robotId": robot_id,
192
+ "skillId": skill_id,
193
+ "source": "hardware",
194
+ "startTime": datetime.fromtimestamp(start_time).isoformat(),
195
+ "endTime": datetime.fromtimestamp(end_time).isoformat(),
196
+ "success": True,
197
+ "metadata": {
198
+ "duration": actual_duration,
199
+ "frameRate": 0,
200
+ "totalFrames": 0,
201
+ "tags": ["demonstration", "cli"],
202
+ "task_description": task_description,
203
+ },
204
+ "frames": [],
205
+ "events": [],
206
+ },
207
+ }
208
+
209
+ if create_labeling_task:
210
+ recording_data["createLabelingTask"] = True
211
+
212
+ response = client._request("POST", "/telemetry/ingest", json=recording_data)
213
+
214
+ artifact_id = response.get("data", {}).get("artifactId", "")
215
+ print(f"\n✓ Uploaded successfully!")
216
+ print(f" Artifact ID: {artifact_id}")
217
+ print(f" View at: https://foodforthought.kindly.fyi/artifacts/{artifact_id}")
218
+
219
+ if create_labeling_task:
220
+ task_id = response.get("data", {}).get("taskId", "")
221
+ if task_id:
222
+ print(f" Labeling Task: https://foodforthought.kindly.fyi/labeling/{task_id}")
223
+
224
+ except Exception as e:
225
+ print(f"\n✗ Upload failed: {e}", file=sys.stderr)
226
+
227
+
228
+ def record_list(client, robot_id: Optional[str] = None, skill_id: Optional[str] = None,
229
+ success_only: bool = False, limit: int = 20) -> None:
230
+ """List telemetry recordings from FoodforThought."""
231
+ print("Fetching recordings...")
232
+
233
+ params = {
234
+ "type": "trajectory",
235
+ "limit": limit,
236
+ }
237
+
238
+ if robot_id:
239
+ params["robotModel"] = robot_id
240
+ if skill_id:
241
+ params["task"] = skill_id
242
+
243
+ try:
244
+ response = client._request("GET", "/artifacts", params=params)
245
+ artifacts = response.get("artifacts", [])
246
+
247
+ if not artifacts:
248
+ print("No recordings found.")
249
+ return
250
+
251
+ print(f"\nFound {len(artifacts)} recording(s):\n")
252
+
253
+ for artifact in artifacts:
254
+ metadata = artifact.get("metadata", {})
255
+
256
+ # Skip failed recordings if success_only
257
+ if success_only and not metadata.get("success", True):
258
+ continue
259
+
260
+ success_marker = "✓" if metadata.get("success", True) else "✗"
261
+ print(f"{success_marker} {artifact.get('name', 'Unnamed')}")
262
+ print(f" ID: {artifact.get('id')}")
263
+ print(f" Robot: {metadata.get('robotId', 'Unknown')}")
264
+ print(f" Skill: {metadata.get('skillId', 'Unknown')}")
265
+ print(f" Duration: {metadata.get('duration', 0):.1f}s")
266
+ print(f" Frames: {metadata.get('frameCount', 0)}")
267
+ print(f" Source: {metadata.get('source', 'Unknown')}")
268
+ print()
269
+
270
+ except Exception as e:
271
+ print(f"Error fetching recordings: {e}", file=sys.stderr)
272
+ sys.exit(1)
273
+
274
+
275
+ def record_local(
276
+ client,
277
+ profile_name: str,
278
+ duration: float = 30.0,
279
+ name: Optional[str] = None,
280
+ fps: float = 5.0,
281
+ detect_trash: bool = False,
282
+ detect_colors: Optional[List[str]] = None,
283
+ output_path: Optional[str] = None,
284
+ upload: bool = True,
285
+ ) -> None:
286
+ """Record from a connected robot with visual capture and detection."""
287
+ from ate.robot.profiles import load_profile, list_profiles
288
+ from ate.robot.manager import RobotManager
289
+ from ate.recording import VisualRecordingSession
290
+
291
+ # Load profile
292
+ profile = load_profile(profile_name)
293
+ if not profile:
294
+ available = list_profiles()
295
+ print(f"Profile '{profile_name}' not found.", file=sys.stderr)
296
+ if available:
297
+ print(f"Available profiles: {', '.join(p.name for p in available)}")
298
+ else:
299
+ print("Run 'ate robot setup' to create a profile first.")
300
+ sys.exit(1)
301
+
302
+ recording_name = name or f"{profile.name}_recording"
303
+ output = output_path or f"{recording_name}.demonstration"
304
+
305
+ print(f"Local Recording")
306
+ print(f"{'=' * 40}")
307
+ print(f"Profile: {profile.name}")
308
+ print(f"Robot type: {profile.robot_type}")
309
+ print(f"Duration: {duration}s")
310
+ print(f"Visual capture: {fps} fps")
311
+ print()
312
+
313
+ # Connect to robot
314
+ print("Connecting to robot...")
315
+ try:
316
+ manager = RobotManager()
317
+ managed = manager.load(profile_name)
318
+ if not managed:
319
+ print(f"Failed to load profile: {profile_name}", file=sys.stderr)
320
+ sys.exit(1)
321
+ if not manager.connect(profile_name):
322
+ print(f"Failed to connect to robot", file=sys.stderr)
323
+ sys.exit(1)
324
+ driver = manager.get(profile_name)
325
+ print(f" Connected to {driver.get_info().name}")
326
+ except Exception as e:
327
+ print(f"Failed to connect: {e}", file=sys.stderr)
328
+ sys.exit(1)
329
+
330
+ # Setup detector
331
+ detector = None
332
+ if detect_trash:
333
+ try:
334
+ from ate.detection import TrashDetector
335
+ detector = TrashDetector()
336
+ print(f" Trash detection enabled")
337
+ except ImportError:
338
+ print(" Warning: Trash detection requires Pillow. Run: pip install 'foodforthought-cli[detection]'")
339
+ elif detect_colors:
340
+ try:
341
+ from ate.detection import ColorDetector
342
+ detector = ColorDetector()
343
+ print(f" Color detection enabled: {', '.join(detect_colors)}")
344
+ except ImportError:
345
+ print(" Warning: Color detection requires Pillow. Run: pip install 'foodforthought-cli[detection]'")
346
+
347
+ # Check if camera is available
348
+ has_camera = hasattr(driver, 'get_image')
349
+ if has_camera:
350
+ print(f" Camera available")
351
+ else:
352
+ print(f" No camera available (recording actions only)")
353
+
354
+ print()
355
+ print("Starting recording in 3 seconds...")
356
+ print("(Control the robot manually during recording)")
357
+ time.sleep(3)
358
+
359
+ # Record
360
+ print()
361
+ print("RECORDING - Press Ctrl+C to stop early")
362
+ print("-" * 40)
363
+
364
+ session = None
365
+ try:
366
+ with VisualRecordingSession(
367
+ driver,
368
+ name=recording_name,
369
+ capture_fps=fps,
370
+ detector=detector,
371
+ ) as session:
372
+ start_time = time.time()
373
+ elapsed = 0
374
+
375
+ while elapsed < duration:
376
+ remaining = duration - elapsed
377
+ frame_info = f", {session.frame_count} frames" if has_camera else ""
378
+ det_info = f", {session.detection_count} detections" if detector else ""
379
+ print(f"\rRecording... {remaining:.0f}s remaining{frame_info}{det_info} ", end="", flush=True)
380
+ time.sleep(0.5)
381
+ elapsed = time.time() - start_time
382
+
383
+ except KeyboardInterrupt:
384
+ print("\n\nRecording stopped by user")
385
+
386
+ if session is None:
387
+ print("No recording session created.", file=sys.stderr)
388
+ manager.disconnect_all()
389
+ sys.exit(1)
390
+
391
+ print("\n")
392
+ print("Recording complete!")
393
+ print("-" * 40)
394
+ print(session.summary())
395
+
396
+ # Save locally
397
+ print()
398
+ print(f"Saving to {output}...")
399
+ session.save(output)
400
+
401
+ if has_camera and session.frame_count > 0:
402
+ frames_path = output.replace(".demonstration", "_frames.json")
403
+ session.save_frames(frames_path, include_data=True)
404
+ print(f"Saved {session.frame_count} frames to {frames_path}")
405
+
406
+ # Upload if requested
407
+ if upload:
408
+ print()
409
+ print("Uploading to FoodforThought...")
410
+ try:
411
+ recording_data = session.to_dict()
412
+ response = client._request("POST", "/telemetry/ingest", json={"recording": recording_data})
413
+ artifact_id = response.get("data", {}).get("artifactId", "")
414
+ print(f"Uploaded successfully!")
415
+ print(f" Artifact ID: {artifact_id}")
416
+ print(f" View at: https://foodforthought.kindly.fyi/artifacts/{artifact_id}")
417
+ except Exception as e:
418
+ print(f"Upload failed: {e}")
419
+ print("Recording saved locally - you can upload later with 'ate upload'")
420
+
421
+ # Disconnect
422
+ manager.disconnect_all()
423
+ print()
424
+ print("Done!")
425
+
426
+
427
+ def register_parser(subparsers):
428
+ """Register recording commands with argparse."""
429
+ record_parser = subparsers.add_parser("record",
430
+ help="Record robot telemetry and demonstrations",
431
+ description="""Record robot telemetry for the data flywheel.
432
+
433
+ EXAMPLES:
434
+ ate record start my-robot-id my-skill-id
435
+ ate record stop --success --notes "Good demo"
436
+ ate record demo my-robot-id pick-object "Pick up the red ball" --duration 60
437
+ ate record local my_robot_profile --duration 30 --detect-trash
438
+ """)
439
+ record_subparsers = record_parser.add_subparsers(dest="record_action")
440
+
441
+ # record start
442
+ record_start_parser = record_subparsers.add_parser("start", help="Start recording telemetry")
443
+ record_start_parser.add_argument("robot", help="Robot ID")
444
+ record_start_parser.add_argument("skill", help="Skill ID being demonstrated")
445
+ record_start_parser.add_argument("-t", "--task", help="Task description")
446
+
447
+ # record stop
448
+ record_stop_parser = record_subparsers.add_parser("stop", help="Stop recording and upload")
449
+ record_stop_parser.add_argument("--success", dest="success", action="store_true",
450
+ default=True, help="Mark recording as successful (default)")
451
+ record_stop_parser.add_argument("--failure", dest="success", action="store_false",
452
+ help="Mark recording as failed")
453
+ record_stop_parser.add_argument("-n", "--notes", help="Recording notes")
454
+ record_stop_parser.add_argument("--no-upload", action="store_true",
455
+ help="Skip uploading to FoodforThought")
456
+ record_stop_parser.add_argument("--create-task", action="store_true",
457
+ help="Create labeling task from this recording")
458
+
459
+ # record status
460
+ record_subparsers.add_parser("status", help="Get current recording status")
461
+
462
+ # record demo
463
+ record_demo_parser = record_subparsers.add_parser("demo", help="Record a timed demonstration")
464
+ record_demo_parser.add_argument("robot", help="Robot ID")
465
+ record_demo_parser.add_argument("skill", help="Skill ID")
466
+ record_demo_parser.add_argument("task", help="Task description")
467
+ record_demo_parser.add_argument("-d", "--duration", type=float, default=30.0,
468
+ help="Recording duration in seconds")
469
+ record_demo_parser.add_argument("--create-task", dest="create_task", action="store_true",
470
+ default=True, help="Create labeling task (default)")
471
+
472
+ # record list
473
+ record_list_parser = record_subparsers.add_parser("list", help="List telemetry recordings")
474
+ record_list_parser.add_argument("-r", "--robot", help="Filter by robot")
475
+ record_list_parser.add_argument("-s", "--skill", help="Filter by skill")
476
+ record_list_parser.add_argument("--success-only", action="store_true",
477
+ help="Only show successful recordings")
478
+ record_list_parser.add_argument("-l", "--limit", type=int, default=20,
479
+ help="Max number of recordings to show")
480
+
481
+ # record local
482
+ record_local_parser = record_subparsers.add_parser("local",
483
+ help="Record from a connected robot with visual capture")
484
+ record_local_parser.add_argument("profile", help="Robot profile name")
485
+ record_local_parser.add_argument("-d", "--duration", type=float, default=30.0,
486
+ help="Recording duration in seconds")
487
+ record_local_parser.add_argument("-n", "--name", help="Recording name")
488
+ record_local_parser.add_argument("--fps", type=float, default=5.0,
489
+ help="Frame capture rate")
490
+ record_local_parser.add_argument("--detect-trash", action="store_true",
491
+ help="Enable trash detection during recording")
492
+ record_local_parser.add_argument("--detect-colors", nargs="+",
493
+ help="Enable color detection (specify colors)")
494
+ record_local_parser.add_argument("-o", "--output", help="Output file path")
495
+ record_local_parser.add_argument("--no-upload", action="store_true",
496
+ help="Skip uploading to FoodforThought")
497
+
498
+
499
+ def handle(client, args):
500
+ """Handle recording commands."""
501
+ if not hasattr(args, 'record_action') or not args.record_action:
502
+ print("Usage: ate record <start|stop|status|demo|list|local>")
503
+ sys.exit(1)
504
+
505
+ if args.record_action == "start":
506
+ record_start(client, args.robot, args.skill, getattr(args, 'task', None))
507
+ elif args.record_action == "stop":
508
+ success = args.success
509
+ upload_flag = not getattr(args, 'no_upload', False)
510
+ record_stop(client, success, getattr(args, 'notes', None),
511
+ upload_flag, getattr(args, 'create_task', False))
512
+ elif args.record_action == "status":
513
+ record_status(client)
514
+ elif args.record_action == "demo":
515
+ record_demo(client, args.robot, args.skill, args.task,
516
+ args.duration, getattr(args, 'create_task', True))
517
+ elif args.record_action == "list":
518
+ record_list(client, getattr(args, 'robot', None), getattr(args, 'skill', None),
519
+ getattr(args, 'success_only', False), getattr(args, 'limit', 20))
520
+ elif args.record_action == "local":
521
+ record_local(client, args.profile, args.duration, getattr(args, 'name', None),
522
+ args.fps, getattr(args, 'detect_trash', False),
523
+ getattr(args, 'detect_colors', None), getattr(args, 'output', None),
524
+ not getattr(args, 'no_upload', False))
ate/commands/repo.py ADDED
@@ -0,0 +1,154 @@
1
+ """
2
+ Repository commands for FoodforThought CLI.
3
+
4
+ Commands:
5
+ - ate init - Initialize a new repository
6
+ - ate clone - Clone a repository
7
+ - ate commit - Create a commit
8
+ - ate push - Push commits to remote
9
+ """
10
+
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Optional, List, Dict
15
+
16
+ import requests
17
+
18
+
19
+ def init_repo(client, name: str, description: str = "", visibility: str = "public") -> Dict:
20
+ """Initialize a new repository."""
21
+ data = {
22
+ "name": name,
23
+ "description": description,
24
+ "visibility": visibility,
25
+ "robotModels": [],
26
+ "taskDomain": None,
27
+ }
28
+ return client._request("POST", "/repositories", json=data)
29
+
30
+
31
+ def clone_repo(client, repo_id: str, target_dir: Optional[str] = None) -> None:
32
+ """Clone a repository."""
33
+ repo = client._request("GET", f"/repositories/{repo_id}")
34
+ repo_data = repo["repository"]
35
+
36
+ if target_dir is None:
37
+ target_dir = repo_data["name"]
38
+
39
+ target_path = Path(target_dir)
40
+ target_path.mkdir(exist_ok=True)
41
+
42
+ # Create .ate directory
43
+ ate_dir = target_path / ".ate"
44
+ ate_dir.mkdir(exist_ok=True)
45
+
46
+ # Save repository metadata
47
+ metadata = {
48
+ "id": repo_data["id"],
49
+ "name": repo_data["name"],
50
+ "owner": repo_data["owner"]["email"],
51
+ "url": f"{client.base_url}/repositories/{repo_data['id']}",
52
+ }
53
+ with open(ate_dir / "config.json", "w") as f:
54
+ json.dump(metadata, f, indent=2)
55
+
56
+ # Download files
57
+ items = repo_data.get("items", [])
58
+ for item in items:
59
+ if item.get("fileStorage"):
60
+ file_url = item["fileStorage"]["url"]
61
+ file_path = target_path / item["filePath"]
62
+
63
+ # Create directory if needed
64
+ file_path.parent.mkdir(parents=True, exist_ok=True)
65
+
66
+ # Download file
67
+ file_response = requests.get(file_url)
68
+ file_response.raise_for_status()
69
+ with open(file_path, "wb") as f:
70
+ f.write(file_response.content)
71
+
72
+ print(f"Cloned repository '{repo_data['name']}' to '{target_dir}'")
73
+
74
+
75
+ def commit(client, message: str, files: Optional[List[str]] = None) -> Dict:
76
+ """Create a commit."""
77
+ # Find .ate directory
78
+ ate_dir = Path(".ate")
79
+ if not ate_dir.exists():
80
+ print("Error: Not a FoodforThought repository. Run 'ate init' first.", file=sys.stderr)
81
+ sys.exit(1)
82
+
83
+ with open(ate_dir / "config.json") as f:
84
+ config = json.load(f)
85
+
86
+ repo_id = config["id"]
87
+
88
+ # Get current files if not specified
89
+ if files is None:
90
+ # This would need to track changes - simplified for now
91
+ files = []
92
+
93
+ # For now, return a placeholder
94
+ # In a full implementation, this would:
95
+ # 1. Track file changes
96
+ # 2. Upload new/modified files
97
+ # 3. Create commit via API
98
+ print(f"Creating commit: {message}")
99
+ print("Note: Full commit functionality requires file tracking implementation")
100
+ return {}
101
+
102
+
103
+ def push(client, branch: str = "main") -> None:
104
+ """Push commits to remote."""
105
+ ate_dir = Path(".ate")
106
+ if not ate_dir.exists():
107
+ print("Error: Not a FoodforThought repository.", file=sys.stderr)
108
+ sys.exit(1)
109
+
110
+ with open(ate_dir / "config.json") as f:
111
+ config = json.load(f)
112
+
113
+ repo_id = config["id"]
114
+ print(f"Pushing to {branch} branch...")
115
+ print("Note: Full push functionality requires commit tracking implementation")
116
+
117
+
118
+ def register_parser(subparsers):
119
+ """Register repo commands with argparse."""
120
+ # init command
121
+ init_parser = subparsers.add_parser("init", help="Initialize a new repository")
122
+ init_parser.add_argument("name", help="Repository name")
123
+ init_parser.add_argument("-d", "--description", default="", help="Repository description")
124
+ init_parser.add_argument(
125
+ "-v", "--visibility", choices=["public", "private"], default="public",
126
+ help="Repository visibility"
127
+ )
128
+
129
+ # clone command
130
+ clone_parser = subparsers.add_parser("clone", help="Clone a repository")
131
+ clone_parser.add_argument("repo_id", help="Repository ID")
132
+ clone_parser.add_argument("target_dir", nargs="?", help="Target directory")
133
+
134
+ # commit command
135
+ commit_parser = subparsers.add_parser("commit", help="Create a commit")
136
+ commit_parser.add_argument("-m", "--message", required=True, help="Commit message")
137
+ commit_parser.add_argument("files", nargs="*", help="Files to commit")
138
+
139
+ # push command
140
+ push_parser = subparsers.add_parser("push", help="Push commits to remote")
141
+ push_parser.add_argument("-b", "--branch", default="main", help="Branch name")
142
+
143
+
144
+ def handle(client, args):
145
+ """Handle repo commands."""
146
+ if args.command == "init":
147
+ result = init_repo(client, args.name, args.description, args.visibility)
148
+ print(f"Created repository: {result['repository']['id']}")
149
+ elif args.command == "clone":
150
+ clone_repo(client, args.repo_id, args.target_dir)
151
+ elif args.command == "commit":
152
+ commit(client, args.message, args.files)
153
+ elif args.command == "push":
154
+ push(client, args.branch)