foodforthought-cli 0.2.7__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 (131) 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 +100 -0
  7. ate/behaviors/approach.py +399 -0
  8. ate/behaviors/common.py +686 -0
  9. ate/behaviors/tree.py +454 -0
  10. ate/cli.py +855 -3995
  11. ate/client.py +90 -0
  12. ate/commands/__init__.py +168 -0
  13. ate/commands/auth.py +389 -0
  14. ate/commands/bridge.py +448 -0
  15. ate/commands/data.py +185 -0
  16. ate/commands/deps.py +111 -0
  17. ate/commands/generate.py +384 -0
  18. ate/commands/memory.py +907 -0
  19. ate/commands/parts.py +166 -0
  20. ate/commands/primitive.py +399 -0
  21. ate/commands/protocol.py +288 -0
  22. ate/commands/recording.py +524 -0
  23. ate/commands/repo.py +154 -0
  24. ate/commands/simulation.py +291 -0
  25. ate/commands/skill.py +303 -0
  26. ate/commands/skills.py +487 -0
  27. ate/commands/team.py +147 -0
  28. ate/commands/workflow.py +271 -0
  29. ate/detection/__init__.py +38 -0
  30. ate/detection/base.py +142 -0
  31. ate/detection/color_detector.py +399 -0
  32. ate/detection/trash_detector.py +322 -0
  33. ate/drivers/__init__.py +39 -0
  34. ate/drivers/ble_transport.py +405 -0
  35. ate/drivers/mechdog.py +942 -0
  36. ate/drivers/wifi_camera.py +477 -0
  37. ate/interfaces/__init__.py +187 -0
  38. ate/interfaces/base.py +273 -0
  39. ate/interfaces/body.py +267 -0
  40. ate/interfaces/detection.py +282 -0
  41. ate/interfaces/locomotion.py +422 -0
  42. ate/interfaces/manipulation.py +408 -0
  43. ate/interfaces/navigation.py +389 -0
  44. ate/interfaces/perception.py +362 -0
  45. ate/interfaces/sensors.py +247 -0
  46. ate/interfaces/types.py +371 -0
  47. ate/llm_proxy.py +239 -0
  48. ate/mcp_server.py +387 -0
  49. ate/memory/__init__.py +35 -0
  50. ate/memory/cloud.py +244 -0
  51. ate/memory/context.py +269 -0
  52. ate/memory/embeddings.py +184 -0
  53. ate/memory/export.py +26 -0
  54. ate/memory/merge.py +146 -0
  55. ate/memory/migrate/__init__.py +34 -0
  56. ate/memory/migrate/base.py +89 -0
  57. ate/memory/migrate/pipeline.py +189 -0
  58. ate/memory/migrate/sources/__init__.py +13 -0
  59. ate/memory/migrate/sources/chroma.py +170 -0
  60. ate/memory/migrate/sources/pinecone.py +120 -0
  61. ate/memory/migrate/sources/qdrant.py +110 -0
  62. ate/memory/migrate/sources/weaviate.py +160 -0
  63. ate/memory/reranker.py +353 -0
  64. ate/memory/search.py +26 -0
  65. ate/memory/store.py +548 -0
  66. ate/recording/__init__.py +83 -0
  67. ate/recording/demonstration.py +378 -0
  68. ate/recording/session.py +415 -0
  69. ate/recording/upload.py +304 -0
  70. ate/recording/visual.py +416 -0
  71. ate/recording/wrapper.py +95 -0
  72. ate/robot/__init__.py +221 -0
  73. ate/robot/agentic_servo.py +856 -0
  74. ate/robot/behaviors.py +493 -0
  75. ate/robot/ble_capture.py +1000 -0
  76. ate/robot/ble_enumerate.py +506 -0
  77. ate/robot/calibration.py +668 -0
  78. ate/robot/calibration_state.py +388 -0
  79. ate/robot/commands.py +3735 -0
  80. ate/robot/direction_calibration.py +554 -0
  81. ate/robot/discovery.py +441 -0
  82. ate/robot/introspection.py +330 -0
  83. ate/robot/llm_system_id.py +654 -0
  84. ate/robot/locomotion_calibration.py +508 -0
  85. ate/robot/manager.py +270 -0
  86. ate/robot/marker_generator.py +611 -0
  87. ate/robot/perception.py +502 -0
  88. ate/robot/primitives.py +614 -0
  89. ate/robot/profiles.py +281 -0
  90. ate/robot/registry.py +322 -0
  91. ate/robot/servo_mapper.py +1153 -0
  92. ate/robot/skill_upload.py +675 -0
  93. ate/robot/target_calibration.py +500 -0
  94. ate/robot/teach.py +515 -0
  95. ate/robot/types.py +242 -0
  96. ate/robot/visual_labeler.py +1048 -0
  97. ate/robot/visual_servo_loop.py +494 -0
  98. ate/robot/visual_servoing.py +570 -0
  99. ate/robot/visual_system_id.py +906 -0
  100. ate/transports/__init__.py +121 -0
  101. ate/transports/base.py +394 -0
  102. ate/transports/ble.py +405 -0
  103. ate/transports/hybrid.py +444 -0
  104. ate/transports/serial.py +345 -0
  105. ate/urdf/__init__.py +30 -0
  106. ate/urdf/capture.py +582 -0
  107. ate/urdf/cloud.py +491 -0
  108. ate/urdf/collision.py +271 -0
  109. ate/urdf/commands.py +708 -0
  110. ate/urdf/depth.py +360 -0
  111. ate/urdf/inertial.py +312 -0
  112. ate/urdf/kinematics.py +330 -0
  113. ate/urdf/lifting.py +415 -0
  114. ate/urdf/meshing.py +300 -0
  115. ate/urdf/models/__init__.py +110 -0
  116. ate/urdf/models/depth_anything.py +253 -0
  117. ate/urdf/models/sam2.py +324 -0
  118. ate/urdf/motion_analysis.py +396 -0
  119. ate/urdf/pipeline.py +468 -0
  120. ate/urdf/scale.py +256 -0
  121. ate/urdf/scan_session.py +411 -0
  122. ate/urdf/segmentation.py +299 -0
  123. ate/urdf/synthesis.py +319 -0
  124. ate/urdf/topology.py +336 -0
  125. ate/urdf/validation.py +371 -0
  126. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
  127. foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
  128. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
  129. foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
  130. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
  131. {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,415 @@
1
+ """
2
+ Recording session for capturing robot demonstrations.
3
+ """
4
+
5
+ import time
6
+ import json
7
+ import uuid
8
+ from datetime import datetime
9
+ from dataclasses import dataclass, field, asdict
10
+ from typing import Any, List, Optional, Dict, Callable
11
+ from functools import wraps
12
+
13
+ from ate.interfaces import RobotInterface, ActionResult
14
+
15
+
16
+ @dataclass
17
+ class RecordedCall:
18
+ """A single recorded interface method call."""
19
+
20
+ timestamp: float # Unix timestamp (seconds)
21
+ relative_time: float # Time since recording started (seconds)
22
+ interface: str # Interface name (e.g., "QuadrupedLocomotion")
23
+ method: str # Method name (e.g., "walk")
24
+ args: tuple # Positional arguments (serialized)
25
+ kwargs: dict # Keyword arguments (serialized)
26
+ result: Any # Return value (serialized)
27
+ success: bool # Whether the call succeeded
28
+ error: Optional[str] = None # Error message if failed
29
+
30
+ def to_dict(self) -> dict:
31
+ """Convert to dictionary for serialization."""
32
+ return {
33
+ "timestamp": self.timestamp,
34
+ "relative_time": self.relative_time,
35
+ "interface": self.interface,
36
+ "method": self.method,
37
+ "args": self._serialize_args(self.args),
38
+ "kwargs": self._serialize_kwargs(self.kwargs),
39
+ "result": self._serialize_result(self.result),
40
+ "success": self.success,
41
+ "error": self.error,
42
+ }
43
+
44
+ def _serialize_args(self, args: tuple) -> list:
45
+ """Serialize arguments to JSON-compatible format."""
46
+ return [self._serialize_value(arg) for arg in args]
47
+
48
+ def _serialize_kwargs(self, kwargs: dict) -> dict:
49
+ """Serialize keyword arguments to JSON-compatible format."""
50
+ return {k: self._serialize_value(v) for k, v in kwargs.items()}
51
+
52
+ def _serialize_value(self, value: Any) -> Any:
53
+ """Serialize a single value."""
54
+ # Handle common types
55
+ if value is None or isinstance(value, (bool, int, float, str)):
56
+ return value
57
+ if isinstance(value, bytes):
58
+ # Don't include large binary data in JSON - just note its size
59
+ if len(value) > 1000:
60
+ return {"__type__": "bytes", "__size__": len(value), "__truncated__": True}
61
+ import base64
62
+ return {"__type__": "bytes", "__b64__": base64.b64encode(value).decode('ascii')}
63
+ if isinstance(value, (list, tuple)):
64
+ return [self._serialize_value(v) for v in value]
65
+ if isinstance(value, dict):
66
+ return {k: self._serialize_value(v) for k, v in value.items()}
67
+
68
+ # Handle our interface types (serialize fields individually to handle bytes)
69
+ if hasattr(value, '__dataclass_fields__'):
70
+ result = {"__type__": type(value).__name__}
71
+ for field_name in value.__dataclass_fields__:
72
+ field_value = getattr(value, field_name)
73
+ result[field_name] = self._serialize_value(field_value)
74
+ return result
75
+ if hasattr(value, 'to_dict'):
76
+ return {"__type__": type(value).__name__, **value.to_dict()}
77
+
78
+ # Handle ActionResult
79
+ if isinstance(value, ActionResult):
80
+ return {
81
+ "__type__": "ActionResult",
82
+ "success": value.success,
83
+ "message": value.message,
84
+ "error": value.error,
85
+ }
86
+
87
+ # Fallback: convert to string
88
+ return {"__type__": type(value).__name__, "__str__": str(value)}
89
+
90
+ def _serialize_result(self, result: Any) -> Any:
91
+ """Serialize return value."""
92
+ return self._serialize_value(result)
93
+
94
+ @classmethod
95
+ def from_dict(cls, data: dict) -> "RecordedCall":
96
+ """Create from dictionary."""
97
+ return cls(
98
+ timestamp=data["timestamp"],
99
+ relative_time=data["relative_time"],
100
+ interface=data["interface"],
101
+ method=data["method"],
102
+ args=tuple(data["args"]),
103
+ kwargs=data["kwargs"],
104
+ result=data["result"],
105
+ success=data["success"],
106
+ error=data.get("error"),
107
+ )
108
+
109
+
110
+ @dataclass
111
+ class RecordingMetadata:
112
+ """Metadata for a recording session."""
113
+
114
+ id: str # Unique recording ID
115
+ name: str # Human-readable name
116
+ robot_name: str # Robot name from driver
117
+ robot_model: str # Robot model from driver
118
+ robot_archetype: str # Robot archetype (quadruped, etc.)
119
+ capabilities: List[str] # Robot capabilities
120
+ start_time: float # Unix timestamp
121
+ end_time: Optional[float] = None # Unix timestamp
122
+ duration: Optional[float] = None # Seconds
123
+ description: Optional[str] = None
124
+ tags: List[str] = field(default_factory=list)
125
+
126
+ def to_dict(self) -> dict:
127
+ return asdict(self)
128
+
129
+ @classmethod
130
+ def from_dict(cls, data: dict) -> "RecordingMetadata":
131
+ return cls(**data)
132
+
133
+
134
+ class RecordingSession:
135
+ """
136
+ Context manager for recording robot demonstrations.
137
+
138
+ Wraps a robot driver to intercept and record all interface method calls.
139
+
140
+ Usage:
141
+ with RecordingSession(driver, name="my_demo") as session:
142
+ driver.stand()
143
+ driver.walk(Vector3.forward(), speed=0.3)
144
+
145
+ session.save("my_demo.demonstration")
146
+ """
147
+
148
+ def __init__(
149
+ self,
150
+ driver: RobotInterface,
151
+ name: str = "recording",
152
+ description: Optional[str] = None,
153
+ tags: Optional[List[str]] = None,
154
+ auto_save: bool = False,
155
+ save_path: Optional[str] = None,
156
+ ):
157
+ """
158
+ Initialize a recording session.
159
+
160
+ Args:
161
+ driver: Robot driver to record
162
+ name: Human-readable name for the recording
163
+ description: Optional description
164
+ tags: Optional tags for categorization
165
+ auto_save: If True, save automatically on exit
166
+ save_path: Path for auto-save (uses name if not provided)
167
+ """
168
+ self._driver = driver
169
+ self._name = name
170
+ self._description = description
171
+ self._tags = tags or []
172
+ self._auto_save = auto_save
173
+ self._save_path = save_path
174
+
175
+ self._calls: List[RecordedCall] = []
176
+ self._start_time: Optional[float] = None
177
+ self._end_time: Optional[float] = None
178
+ self._original_methods: Dict[str, Callable] = {}
179
+ self._recording = False
180
+
181
+ @property
182
+ def calls(self) -> List[RecordedCall]:
183
+ """Get all recorded calls."""
184
+ return self._calls.copy()
185
+
186
+ @property
187
+ def duration(self) -> Optional[float]:
188
+ """Get recording duration in seconds."""
189
+ if self._start_time is None:
190
+ return None
191
+ end = self._end_time or time.time()
192
+ return end - self._start_time
193
+
194
+ @property
195
+ def is_recording(self) -> bool:
196
+ """Check if currently recording."""
197
+ return self._recording
198
+
199
+ def __enter__(self) -> "RecordingSession":
200
+ """Start recording."""
201
+ self.start()
202
+ return self
203
+
204
+ def __exit__(self, exc_type, exc_val, exc_tb):
205
+ """Stop recording."""
206
+ self.stop()
207
+ if self._auto_save:
208
+ path = self._save_path or f"{self._name}.demonstration"
209
+ self.save(path)
210
+ return False # Don't suppress exceptions
211
+
212
+ def start(self) -> None:
213
+ """Start recording interface calls."""
214
+ if self._recording:
215
+ return
216
+
217
+ self._start_time = time.time()
218
+ self._recording = True
219
+ self._wrap_driver_methods()
220
+
221
+ def stop(self) -> None:
222
+ """Stop recording interface calls."""
223
+ if not self._recording:
224
+ return
225
+
226
+ self._end_time = time.time()
227
+ self._recording = False
228
+ self._unwrap_driver_methods()
229
+
230
+ def _wrap_driver_methods(self) -> None:
231
+ """Wrap all driver methods to intercept calls."""
232
+ # Get all interfaces the driver implements
233
+ interfaces = self._get_driver_interfaces()
234
+
235
+ for interface_name, interface_cls in interfaces.items():
236
+ # Get methods defined in this interface
237
+ for method_name in self._get_interface_methods(interface_cls):
238
+ if hasattr(self._driver, method_name):
239
+ original = getattr(self._driver, method_name)
240
+ if callable(original) and not method_name.startswith('_'):
241
+ # Store original
242
+ key = f"{interface_name}.{method_name}"
243
+ self._original_methods[key] = original
244
+
245
+ # Create wrapper
246
+ wrapper = self._create_wrapper(interface_name, method_name, original)
247
+ setattr(self._driver, method_name, wrapper)
248
+
249
+ def _unwrap_driver_methods(self) -> None:
250
+ """Restore original driver methods."""
251
+ for key, original in self._original_methods.items():
252
+ _, method_name = key.split(".", 1)
253
+ setattr(self._driver, method_name, original)
254
+ self._original_methods.clear()
255
+
256
+ def _get_driver_interfaces(self) -> Dict[str, type]:
257
+ """Get all interface classes the driver implements."""
258
+ from ate.interfaces import (
259
+ RobotInterface, SafetyInterface,
260
+ QuadrupedLocomotion, BipedLocomotion, WheeledLocomotion, AerialLocomotion,
261
+ ArmInterface, GripperInterface, DualArmInterface,
262
+ CameraInterface, DepthCameraInterface, LidarInterface, IMUInterface, ForceTorqueInterface,
263
+ BodyPoseInterface,
264
+ )
265
+
266
+ all_interfaces = {
267
+ "RobotInterface": RobotInterface,
268
+ "SafetyInterface": SafetyInterface,
269
+ "QuadrupedLocomotion": QuadrupedLocomotion,
270
+ "BipedLocomotion": BipedLocomotion,
271
+ "WheeledLocomotion": WheeledLocomotion,
272
+ "AerialLocomotion": AerialLocomotion,
273
+ "ArmInterface": ArmInterface,
274
+ "GripperInterface": GripperInterface,
275
+ "DualArmInterface": DualArmInterface,
276
+ "CameraInterface": CameraInterface,
277
+ "DepthCameraInterface": DepthCameraInterface,
278
+ "LidarInterface": LidarInterface,
279
+ "IMUInterface": IMUInterface,
280
+ "ForceTorqueInterface": ForceTorqueInterface,
281
+ "BodyPoseInterface": BodyPoseInterface,
282
+ }
283
+
284
+ return {
285
+ name: cls for name, cls in all_interfaces.items()
286
+ if isinstance(self._driver, cls)
287
+ }
288
+
289
+ def _get_interface_methods(self, interface_cls: type) -> List[str]:
290
+ """Get public methods defined in an interface class."""
291
+ methods = []
292
+ for name in dir(interface_cls):
293
+ if not name.startswith('_'):
294
+ attr = getattr(interface_cls, name, None)
295
+ if callable(attr):
296
+ methods.append(name)
297
+ return methods
298
+
299
+ def _create_wrapper(
300
+ self,
301
+ interface_name: str,
302
+ method_name: str,
303
+ original: Callable
304
+ ) -> Callable:
305
+ """Create a wrapper function that records the call."""
306
+
307
+ @wraps(original)
308
+ def wrapper(*args, **kwargs):
309
+ timestamp = time.time()
310
+ relative_time = timestamp - self._start_time
311
+
312
+ try:
313
+ result = original(*args, **kwargs)
314
+ success = True
315
+ error = None
316
+
317
+ # Check ActionResult for failure
318
+ if isinstance(result, ActionResult) and not result.success:
319
+ error = result.error
320
+
321
+ except Exception as e:
322
+ result = None
323
+ success = False
324
+ error = str(e)
325
+ raise
326
+ finally:
327
+ # Record the call
328
+ call = RecordedCall(
329
+ timestamp=timestamp,
330
+ relative_time=relative_time,
331
+ interface=interface_name,
332
+ method=method_name,
333
+ args=args,
334
+ kwargs=kwargs,
335
+ result=result,
336
+ success=success,
337
+ error=error,
338
+ )
339
+ self._calls.append(call)
340
+
341
+ return result
342
+
343
+ return wrapper
344
+
345
+ def get_metadata(self) -> RecordingMetadata:
346
+ """Get recording metadata."""
347
+ info = self._driver.get_info()
348
+ return RecordingMetadata(
349
+ id=str(uuid.uuid4()),
350
+ name=self._name,
351
+ robot_name=info.name,
352
+ robot_model=info.model,
353
+ robot_archetype=info.archetype,
354
+ capabilities=[c.name for c in info.capabilities],
355
+ start_time=self._start_time or 0,
356
+ end_time=self._end_time,
357
+ duration=self.duration,
358
+ description=self._description,
359
+ tags=self._tags,
360
+ )
361
+
362
+ def save(self, path: str) -> None:
363
+ """
364
+ Save recording to a file.
365
+
366
+ Args:
367
+ path: File path (should end with .demonstration)
368
+ """
369
+ data = {
370
+ "version": "1.0",
371
+ "metadata": self.get_metadata().to_dict(),
372
+ "calls": [call.to_dict() for call in self._calls],
373
+ }
374
+
375
+ with open(path, 'w') as f:
376
+ json.dump(data, f, indent=2)
377
+
378
+ def to_dict(self) -> dict:
379
+ """Convert to dictionary for API upload."""
380
+ return {
381
+ "version": "1.0",
382
+ "metadata": self.get_metadata().to_dict(),
383
+ "calls": [call.to_dict() for call in self._calls],
384
+ }
385
+
386
+ def summary(self) -> str:
387
+ """Get a human-readable summary of the recording."""
388
+ if not self._calls:
389
+ return "No calls recorded"
390
+
391
+ # Group by interface
392
+ by_interface: Dict[str, List[RecordedCall]] = {}
393
+ for call in self._calls:
394
+ if call.interface not in by_interface:
395
+ by_interface[call.interface] = []
396
+ by_interface[call.interface].append(call)
397
+
398
+ lines = [
399
+ f"Recording: {self._name}",
400
+ f"Duration: {self.duration:.2f}s" if self.duration else "Duration: N/A",
401
+ f"Total calls: {len(self._calls)}",
402
+ "",
403
+ "By interface:",
404
+ ]
405
+
406
+ for interface, calls in by_interface.items():
407
+ lines.append(f" {interface}: {len(calls)} calls")
408
+ # Count by method
409
+ by_method: Dict[str, int] = {}
410
+ for call in calls:
411
+ by_method[call.method] = by_method.get(call.method, 0) + 1
412
+ for method, count in sorted(by_method.items()):
413
+ lines.append(f" - {method}: {count}")
414
+
415
+ return "\n".join(lines)