foodforthought-cli 0.2.1__py3-none-any.whl → 0.2.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.
Files changed (46) hide show
  1. ate/__init__.py +1 -1
  2. ate/bridge_server.py +622 -0
  3. ate/cli.py +2625 -242
  4. ate/compatibility.py +580 -0
  5. ate/generators/__init__.py +19 -0
  6. ate/generators/docker_generator.py +461 -0
  7. ate/generators/hardware_config.py +469 -0
  8. ate/generators/ros2_generator.py +617 -0
  9. ate/generators/skill_generator.py +783 -0
  10. ate/marketplace.py +524 -0
  11. ate/mcp_server.py +1341 -107
  12. ate/primitives.py +1016 -0
  13. ate/robot_setup.py +2222 -0
  14. ate/skill_schema.py +537 -0
  15. ate/telemetry/__init__.py +33 -0
  16. ate/telemetry/cli.py +455 -0
  17. ate/telemetry/collector.py +444 -0
  18. ate/telemetry/context.py +318 -0
  19. ate/telemetry/fleet_agent.py +419 -0
  20. ate/telemetry/formats/__init__.py +18 -0
  21. ate/telemetry/formats/hdf5_serializer.py +503 -0
  22. ate/telemetry/formats/mcap_serializer.py +457 -0
  23. ate/telemetry/types.py +334 -0
  24. foodforthought_cli-0.2.3.dist-info/METADATA +300 -0
  25. foodforthought_cli-0.2.3.dist-info/RECORD +44 -0
  26. foodforthought_cli-0.2.3.dist-info/top_level.txt +6 -0
  27. mechdog_labeled/__init__.py +3 -0
  28. mechdog_labeled/primitives.py +113 -0
  29. mechdog_labeled/servo_map.py +209 -0
  30. mechdog_output/__init__.py +3 -0
  31. mechdog_output/primitives.py +59 -0
  32. mechdog_output/servo_map.py +203 -0
  33. test_autodetect/__init__.py +3 -0
  34. test_autodetect/primitives.py +113 -0
  35. test_autodetect/servo_map.py +209 -0
  36. test_full_auto/__init__.py +3 -0
  37. test_full_auto/primitives.py +113 -0
  38. test_full_auto/servo_map.py +209 -0
  39. test_smart_detect/__init__.py +3 -0
  40. test_smart_detect/primitives.py +113 -0
  41. test_smart_detect/servo_map.py +209 -0
  42. foodforthought_cli-0.2.1.dist-info/METADATA +0 -151
  43. foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
  44. foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
  45. {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/WHEEL +0 -0
  46. {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.3.dist-info}/entry_points.txt +0 -0
ate/skill_schema.py ADDED
@@ -0,0 +1,537 @@
1
+ """
2
+ Skill Schema - Data classes for skill specifications.
3
+
4
+ This module defines the complete schema for robot skill specifications,
5
+ which are the input to the Skill Compiler.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from pathlib import Path
11
+ from typing import Any, Dict, List, Optional, Tuple, Union
12
+ import json
13
+ import yaml
14
+
15
+
16
+ class ParameterType(str, Enum):
17
+ """Supported parameter types for skill definitions."""
18
+ POSE = "Pose"
19
+ FLOAT = "float"
20
+ INT = "int"
21
+ BOOL = "bool"
22
+ STRING = "string"
23
+ ARRAY = "array"
24
+ JOINT_STATE = "JointState"
25
+ TRAJECTORY = "Trajectory"
26
+ POINT = "Point"
27
+ QUATERNION = "Quaternion"
28
+ TRANSFORM = "Transform"
29
+
30
+
31
+ class HardwareType(str, Enum):
32
+ """Types of hardware components."""
33
+ ARM = "arm"
34
+ GRIPPER = "gripper"
35
+ MOBILE_BASE = "mobile_base"
36
+ SENSOR = "sensor"
37
+ CAMERA = "camera"
38
+ FORCE_SENSOR = "force_sensor"
39
+ LIDAR = "lidar"
40
+ IMU = "imu"
41
+
42
+
43
+ class ComparisonOperator(str, Enum):
44
+ """Operators for hardware constraints."""
45
+ EQ = "=="
46
+ NE = "!="
47
+ GT = ">"
48
+ GE = ">="
49
+ LT = "<"
50
+ LE = "<="
51
+
52
+
53
+ @dataclass
54
+ class SkillParameter:
55
+ """
56
+ Definition of a skill parameter.
57
+
58
+ Parameters define the inputs a skill accepts, their types,
59
+ defaults, and constraints.
60
+ """
61
+ name: str
62
+ type: str # ParameterType value as string
63
+ description: str
64
+ default: Optional[Any] = None
65
+ range: Optional[Tuple[Any, Any]] = None
66
+ required: bool = True
67
+ units: Optional[str] = None # e.g., "meters", "radians", "newtons"
68
+
69
+ def validate_value(self, value: Any) -> List[str]:
70
+ """Validate a value against this parameter's constraints."""
71
+ errors = []
72
+
73
+ if value is None:
74
+ if self.required and self.default is None:
75
+ errors.append(f"Parameter '{self.name}' is required but not provided")
76
+ return errors
77
+
78
+ # Type validation
79
+ type_validators = {
80
+ "float": lambda v: isinstance(v, (int, float)),
81
+ "int": lambda v: isinstance(v, int),
82
+ "bool": lambda v: isinstance(v, bool),
83
+ "string": lambda v: isinstance(v, str),
84
+ "array": lambda v: isinstance(v, (list, tuple)),
85
+ "Pose": lambda v: isinstance(v, dict) and all(
86
+ k in v for k in ["position", "orientation"]
87
+ ),
88
+ }
89
+
90
+ validator = type_validators.get(self.type)
91
+ if validator and not validator(value):
92
+ errors.append(
93
+ f"Parameter '{self.name}' expected type {self.type}, "
94
+ f"got {type(value).__name__}"
95
+ )
96
+
97
+ # Range validation
98
+ if self.range and isinstance(value, (int, float)):
99
+ min_val, max_val = self.range
100
+ if value < min_val or value > max_val:
101
+ errors.append(
102
+ f"Parameter '{self.name}' value {value} out of range "
103
+ f"[{min_val}, {max_val}]"
104
+ )
105
+
106
+ return errors
107
+
108
+ def to_dict(self) -> Dict[str, Any]:
109
+ """Convert to dictionary representation."""
110
+ d = {
111
+ "type": self.type,
112
+ "description": self.description,
113
+ }
114
+ if self.default is not None:
115
+ d["default"] = self.default
116
+ if self.range is not None:
117
+ d["range"] = list(self.range)
118
+ if not self.required:
119
+ d["required"] = False
120
+ if self.units:
121
+ d["units"] = self.units
122
+ return d
123
+
124
+ @classmethod
125
+ def from_dict(cls, name: str, data: Dict[str, Any]) -> "SkillParameter":
126
+ """Create from dictionary representation."""
127
+ range_val = data.get("range")
128
+ if range_val:
129
+ range_val = tuple(range_val)
130
+
131
+ return cls(
132
+ name=name,
133
+ type=data["type"],
134
+ description=data.get("description", ""),
135
+ default=data.get("default"),
136
+ range=range_val,
137
+ required=data.get("required", True),
138
+ units=data.get("units"),
139
+ )
140
+
141
+
142
+ @dataclass
143
+ class HardwareConstraint:
144
+ """A single constraint on hardware (e.g., dof >= 6)."""
145
+ property: str
146
+ operator: str # ComparisonOperator value
147
+ value: Any
148
+
149
+ def evaluate(self, actual_value: Any) -> bool:
150
+ """Evaluate this constraint against an actual value."""
151
+ ops = {
152
+ "==": lambda a, b: a == b,
153
+ "!=": lambda a, b: a != b,
154
+ ">": lambda a, b: a > b,
155
+ ">=": lambda a, b: a >= b,
156
+ "<": lambda a, b: a < b,
157
+ "<=": lambda a, b: a <= b,
158
+ }
159
+
160
+ # Handle string comparison operators like ">=6"
161
+ op = self.operator
162
+ if op not in ops:
163
+ # Try to parse from value string like ">=6"
164
+ if isinstance(self.value, str):
165
+ for test_op in [">=", "<=", "!=", "==", ">", "<"]:
166
+ if self.value.startswith(test_op):
167
+ op = test_op
168
+ self.value = self._parse_value(self.value[len(test_op):])
169
+ break
170
+
171
+ op_func = ops.get(op, ops["=="])
172
+ try:
173
+ return op_func(actual_value, self.value)
174
+ except TypeError:
175
+ return False
176
+
177
+ def _parse_value(self, val_str: str) -> Any:
178
+ """Parse a value string to appropriate type."""
179
+ val_str = val_str.strip()
180
+ # Try int
181
+ try:
182
+ return int(val_str)
183
+ except ValueError:
184
+ pass
185
+ # Try float
186
+ try:
187
+ return float(val_str)
188
+ except ValueError:
189
+ pass
190
+ # Return as string
191
+ return val_str
192
+
193
+
194
+ @dataclass
195
+ class HardwareRequirement:
196
+ """
197
+ Hardware requirement for a skill.
198
+
199
+ Defines what hardware components a skill needs and their constraints.
200
+ """
201
+ component_type: str # HardwareType value as string
202
+ constraints: Dict[str, Any] = field(default_factory=dict)
203
+ optional: bool = False
204
+
205
+ def get_constraints(self) -> List[HardwareConstraint]:
206
+ """Parse constraints into HardwareConstraint objects."""
207
+ result = []
208
+ for prop, value in self.constraints.items():
209
+ if isinstance(value, str) and any(
210
+ value.startswith(op) for op in [">=", "<=", "!=", "==", ">", "<"]
211
+ ):
212
+ # Parse operator from value
213
+ for op in [">=", "<=", "!=", "==", ">", "<"]:
214
+ if value.startswith(op):
215
+ result.append(HardwareConstraint(
216
+ property=prop,
217
+ operator=op,
218
+ value=value[len(op):].strip()
219
+ ))
220
+ break
221
+ else:
222
+ # Equality constraint
223
+ result.append(HardwareConstraint(
224
+ property=prop,
225
+ operator="==",
226
+ value=value
227
+ ))
228
+ return result
229
+
230
+ def to_dict(self) -> Dict[str, Any]:
231
+ """Convert to dictionary representation."""
232
+ return {self.component_type: self.constraints}
233
+
234
+ @classmethod
235
+ def from_dict(cls, data: Dict[str, Any]) -> "HardwareRequirement":
236
+ """Create from dictionary representation."""
237
+ # Handle format: {"arm": {"dof": ">=6"}}
238
+ if len(data) == 1:
239
+ component_type = list(data.keys())[0]
240
+ constraints = data[component_type]
241
+ if isinstance(constraints, dict):
242
+ return cls(
243
+ component_type=component_type,
244
+ constraints=constraints
245
+ )
246
+
247
+ # Handle format: {"component_type": "arm", "constraints": {...}}
248
+ return cls(
249
+ component_type=data.get("component_type", "unknown"),
250
+ constraints=data.get("constraints", {}),
251
+ optional=data.get("optional", False)
252
+ )
253
+
254
+
255
+ @dataclass
256
+ class SuccessCriterion:
257
+ """
258
+ Success criterion for skill completion.
259
+
260
+ Defines how to determine if a skill has succeeded.
261
+ """
262
+ name: str
263
+ condition: str # Condition expression
264
+ tolerance: Optional[float] = None
265
+ description: Optional[str] = None
266
+
267
+ def to_dict(self) -> Dict[str, Any]:
268
+ """Convert to dictionary representation."""
269
+ d = {"condition": self.condition}
270
+ if self.tolerance is not None:
271
+ d["tolerance"] = self.tolerance
272
+ if self.description:
273
+ d["description"] = self.description
274
+ return d
275
+
276
+ @classmethod
277
+ def from_dict(cls, name: str, data: Dict[str, Any]) -> "SuccessCriterion":
278
+ """Create from dictionary representation."""
279
+ if isinstance(data, str):
280
+ # Simple format: "object_at_place_pose"
281
+ return cls(name=name, condition=data)
282
+
283
+ return cls(
284
+ name=name,
285
+ condition=data.get("condition", ""),
286
+ tolerance=data.get("tolerance"),
287
+ description=data.get("description")
288
+ )
289
+
290
+
291
+ @dataclass
292
+ class PrimitiveCall:
293
+ """
294
+ A call to a primitive within a skill's execution flow.
295
+ """
296
+ primitive: str
297
+ parameters: Dict[str, Any] = field(default_factory=dict)
298
+ condition: Optional[str] = None # Optional condition to execute
299
+ on_failure: Optional[str] = None # Action on failure: "abort", "retry", "continue"
300
+ retries: int = 0
301
+
302
+
303
+ @dataclass
304
+ class SkillSpecification:
305
+ """
306
+ Complete specification for a robot skill.
307
+
308
+ This is the main data structure that the Skill Compiler uses
309
+ to generate deployable skill packages.
310
+ """
311
+ name: str
312
+ version: str
313
+ description: str
314
+ parameters: List[SkillParameter]
315
+ primitives: List[str] # List of primitive names used
316
+ hardware_requirements: List[HardwareRequirement]
317
+ success_criteria: List[SuccessCriterion]
318
+
319
+ # Optional fields
320
+ author: Optional[str] = None
321
+ license: Optional[str] = None
322
+ category: Optional[str] = None
323
+ tags: List[str] = field(default_factory=list)
324
+ dependencies: List[str] = field(default_factory=list) # Other skills
325
+ execution_flow: List[PrimitiveCall] = field(default_factory=list)
326
+
327
+ # Safety constraints
328
+ max_velocity: Optional[float] = None # m/s
329
+ max_force: Optional[float] = None # N
330
+ workspace_bounds: Optional[Dict[str, Tuple[float, float]]] = None
331
+
332
+ @classmethod
333
+ def from_yaml(cls, path: Union[str, Path]) -> "SkillSpecification":
334
+ """Load skill specification from YAML file."""
335
+ path = Path(path)
336
+ if not path.exists():
337
+ raise FileNotFoundError(f"Skill specification not found: {path}")
338
+
339
+ with open(path, "r") as f:
340
+ data = yaml.safe_load(f)
341
+
342
+ return cls.from_dict(data)
343
+
344
+ @classmethod
345
+ def from_dict(cls, data: Dict[str, Any]) -> "SkillSpecification":
346
+ """Create skill specification from dictionary."""
347
+ # Parse parameters
348
+ parameters = []
349
+ params_data = data.get("parameters", {})
350
+ for name, param_data in params_data.items():
351
+ parameters.append(SkillParameter.from_dict(name, param_data))
352
+
353
+ # Parse hardware requirements
354
+ hardware_requirements = []
355
+ hw_data = data.get("hardware_requirements", [])
356
+ for hw in hw_data:
357
+ hardware_requirements.append(HardwareRequirement.from_dict(hw))
358
+
359
+ # Parse success criteria
360
+ success_criteria = []
361
+ criteria_data = data.get("success_criteria", {})
362
+ for name, criterion_data in criteria_data.items():
363
+ success_criteria.append(SuccessCriterion.from_dict(name, criterion_data))
364
+
365
+ # Parse execution flow
366
+ execution_flow = []
367
+ flow_data = data.get("execution_flow", [])
368
+ for step in flow_data:
369
+ if isinstance(step, str):
370
+ execution_flow.append(PrimitiveCall(primitive=step))
371
+ else:
372
+ execution_flow.append(PrimitiveCall(
373
+ primitive=step.get("primitive", ""),
374
+ parameters=step.get("parameters", {}),
375
+ condition=step.get("condition"),
376
+ on_failure=step.get("on_failure"),
377
+ retries=step.get("retries", 0)
378
+ ))
379
+
380
+ # Parse workspace bounds
381
+ workspace_bounds = None
382
+ if "workspace_bounds" in data:
383
+ wb = data["workspace_bounds"]
384
+ workspace_bounds = {
385
+ axis: tuple(bounds) for axis, bounds in wb.items()
386
+ }
387
+
388
+ return cls(
389
+ name=data["name"],
390
+ version=data.get("version", "1.0.0"),
391
+ description=data.get("description", ""),
392
+ parameters=parameters,
393
+ primitives=data.get("primitives", []),
394
+ hardware_requirements=hardware_requirements,
395
+ success_criteria=success_criteria,
396
+ author=data.get("author"),
397
+ license=data.get("license"),
398
+ category=data.get("category"),
399
+ tags=data.get("tags", []),
400
+ dependencies=data.get("dependencies", []),
401
+ execution_flow=execution_flow,
402
+ max_velocity=data.get("max_velocity"),
403
+ max_force=data.get("max_force"),
404
+ workspace_bounds=workspace_bounds,
405
+ )
406
+
407
+ def to_dict(self) -> Dict[str, Any]:
408
+ """Convert to dictionary representation."""
409
+ d = {
410
+ "name": self.name,
411
+ "version": self.version,
412
+ "description": self.description,
413
+ "parameters": {p.name: p.to_dict() for p in self.parameters},
414
+ "primitives": self.primitives,
415
+ "hardware_requirements": [hr.to_dict() for hr in self.hardware_requirements],
416
+ "success_criteria": {sc.name: sc.to_dict() for sc in self.success_criteria},
417
+ }
418
+
419
+ if self.author:
420
+ d["author"] = self.author
421
+ if self.license:
422
+ d["license"] = self.license
423
+ if self.category:
424
+ d["category"] = self.category
425
+ if self.tags:
426
+ d["tags"] = self.tags
427
+ if self.dependencies:
428
+ d["dependencies"] = self.dependencies
429
+ if self.execution_flow:
430
+ d["execution_flow"] = [
431
+ {
432
+ "primitive": pc.primitive,
433
+ "parameters": pc.parameters,
434
+ "condition": pc.condition,
435
+ "on_failure": pc.on_failure,
436
+ "retries": pc.retries
437
+ } if pc.parameters or pc.condition else pc.primitive
438
+ for pc in self.execution_flow
439
+ ]
440
+ if self.max_velocity:
441
+ d["max_velocity"] = self.max_velocity
442
+ if self.max_force:
443
+ d["max_force"] = self.max_force
444
+ if self.workspace_bounds:
445
+ d["workspace_bounds"] = {
446
+ k: list(v) for k, v in self.workspace_bounds.items()
447
+ }
448
+
449
+ return d
450
+
451
+ def to_yaml(self, path: Union[str, Path]) -> None:
452
+ """Save skill specification to YAML file."""
453
+ path = Path(path)
454
+ path.parent.mkdir(parents=True, exist_ok=True)
455
+
456
+ with open(path, "w") as f:
457
+ yaml.dump(self.to_dict(), f, default_flow_style=False, sort_keys=False)
458
+
459
+ def validate(self) -> List[str]:
460
+ """
461
+ Validate the skill specification.
462
+
463
+ Returns a list of validation errors, or empty list if valid.
464
+ """
465
+ errors = []
466
+
467
+ # Name validation
468
+ if not self.name:
469
+ errors.append("Skill name cannot be empty")
470
+ elif not self.name.replace("_", "").replace("-", "").isalnum():
471
+ errors.append(
472
+ f"Skill name '{self.name}' contains invalid characters. "
473
+ "Use only alphanumeric characters, underscores, and hyphens."
474
+ )
475
+
476
+ # Version validation
477
+ if not self.version:
478
+ errors.append("Version cannot be empty")
479
+ else:
480
+ # Simple semver check
481
+ parts = self.version.split(".")
482
+ if len(parts) != 3:
483
+ errors.append(
484
+ f"Version '{self.version}' should be in semver format (x.y.z)"
485
+ )
486
+
487
+ # Parameters validation
488
+ param_names = set()
489
+ for param in self.parameters:
490
+ if param.name in param_names:
491
+ errors.append(f"Duplicate parameter name: {param.name}")
492
+ param_names.add(param.name)
493
+
494
+ if not param.type:
495
+ errors.append(f"Parameter '{param.name}' missing type")
496
+
497
+ # Primitives validation
498
+ if not self.primitives:
499
+ errors.append("At least one primitive is required")
500
+
501
+ # Hardware requirements validation
502
+ if not self.hardware_requirements:
503
+ errors.append("At least one hardware requirement is required")
504
+
505
+ # Success criteria validation
506
+ if not self.success_criteria:
507
+ errors.append("At least one success criterion is required")
508
+
509
+ return errors
510
+
511
+ def get_parameter(self, name: str) -> Optional[SkillParameter]:
512
+ """Get a parameter by name."""
513
+ for param in self.parameters:
514
+ if param.name == name:
515
+ return param
516
+ return None
517
+
518
+ def get_required_hardware_types(self) -> List[str]:
519
+ """Get list of required hardware component types."""
520
+ return [
521
+ req.component_type
522
+ for req in self.hardware_requirements
523
+ if not req.optional
524
+ ]
525
+
526
+ def __repr__(self) -> str:
527
+ return (
528
+ f"SkillSpecification(name='{self.name}', version='{self.version}', "
529
+ f"primitives={self.primitives}, "
530
+ f"parameters={[p.name for p in self.parameters]})"
531
+ )
532
+
533
+
534
+ # Convenience type aliases
535
+ SkillSpec = SkillSpecification
536
+ Param = SkillParameter
537
+ HWReq = HardwareRequirement
@@ -0,0 +1,33 @@
1
+ """
2
+ Telemetry module for FoodforThought CLI
3
+
4
+ Provides tools for collecting, serializing, and uploading robot telemetry data
5
+ from simulations and hardware executions.
6
+ """
7
+
8
+ from .types import (
9
+ Pose,
10
+ Contact,
11
+ TrajectoryFrame,
12
+ TrajectoryMetadata,
13
+ TrajectoryRecording,
14
+ ExecutionEvent,
15
+ SensorReading,
16
+ )
17
+ from .collector import TelemetryCollector
18
+ from .context import record_trajectory
19
+
20
+ __all__ = [
21
+ # Types
22
+ "Pose",
23
+ "Contact",
24
+ "TrajectoryFrame",
25
+ "TrajectoryMetadata",
26
+ "TrajectoryRecording",
27
+ "ExecutionEvent",
28
+ "SensorReading",
29
+ # Collector
30
+ "TelemetryCollector",
31
+ # Context manager
32
+ "record_trajectory",
33
+ ]