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.
- ate/__init__.py +6 -0
- ate/__main__.py +16 -0
- ate/auth/__init__.py +1 -0
- ate/auth/device_flow.py +141 -0
- ate/auth/token_store.py +96 -0
- ate/behaviors/__init__.py +12 -0
- ate/behaviors/approach.py +399 -0
- ate/cli.py +855 -4551
- ate/client.py +90 -0
- ate/commands/__init__.py +168 -0
- ate/commands/auth.py +389 -0
- ate/commands/bridge.py +448 -0
- ate/commands/data.py +185 -0
- ate/commands/deps.py +111 -0
- ate/commands/generate.py +384 -0
- ate/commands/memory.py +907 -0
- ate/commands/parts.py +166 -0
- ate/commands/primitive.py +399 -0
- ate/commands/protocol.py +288 -0
- ate/commands/recording.py +524 -0
- ate/commands/repo.py +154 -0
- ate/commands/simulation.py +291 -0
- ate/commands/skill.py +303 -0
- ate/commands/skills.py +487 -0
- ate/commands/team.py +147 -0
- ate/commands/workflow.py +271 -0
- ate/detection/__init__.py +38 -0
- ate/detection/base.py +142 -0
- ate/detection/color_detector.py +399 -0
- ate/detection/trash_detector.py +322 -0
- ate/drivers/__init__.py +18 -6
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +360 -24
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +16 -0
- ate/interfaces/base.py +2 -0
- ate/interfaces/sensors.py +247 -0
- ate/llm_proxy.py +239 -0
- ate/memory/__init__.py +35 -0
- ate/memory/cloud.py +244 -0
- ate/memory/context.py +269 -0
- ate/memory/embeddings.py +184 -0
- ate/memory/export.py +26 -0
- ate/memory/merge.py +146 -0
- ate/memory/migrate/__init__.py +34 -0
- ate/memory/migrate/base.py +89 -0
- ate/memory/migrate/pipeline.py +189 -0
- ate/memory/migrate/sources/__init__.py +13 -0
- ate/memory/migrate/sources/chroma.py +170 -0
- ate/memory/migrate/sources/pinecone.py +120 -0
- ate/memory/migrate/sources/qdrant.py +110 -0
- ate/memory/migrate/sources/weaviate.py +160 -0
- ate/memory/reranker.py +353 -0
- ate/memory/search.py +26 -0
- ate/memory/store.py +548 -0
- ate/recording/__init__.py +42 -3
- ate/recording/session.py +12 -2
- ate/recording/visual.py +416 -0
- ate/robot/__init__.py +142 -0
- ate/robot/agentic_servo.py +856 -0
- ate/robot/behaviors.py +493 -0
- ate/robot/ble_capture.py +1000 -0
- ate/robot/ble_enumerate.py +506 -0
- ate/robot/calibration.py +88 -3
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +143 -11
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +104 -2
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +6 -0
- ate/robot/registry.py +5 -2
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +285 -3
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +9 -0
- ate/robot/visual_servo_loop.py +494 -0
- ate/robot/visual_servoing.py +570 -0
- ate/robot/visual_system_id.py +906 -0
- ate/transports/__init__.py +121 -0
- ate/transports/base.py +394 -0
- ate/transports/ble.py +405 -0
- ate/transports/hybrid.py +444 -0
- ate/transports/serial.py +345 -0
- ate/urdf/__init__.py +30 -0
- ate/urdf/capture.py +582 -0
- ate/urdf/cloud.py +491 -0
- ate/urdf/collision.py +271 -0
- ate/urdf/commands.py +708 -0
- ate/urdf/depth.py +360 -0
- ate/urdf/inertial.py +312 -0
- ate/urdf/kinematics.py +330 -0
- ate/urdf/lifting.py +415 -0
- ate/urdf/meshing.py +300 -0
- ate/urdf/models/__init__.py +110 -0
- ate/urdf/models/depth_anything.py +253 -0
- ate/urdf/models/sam2.py +324 -0
- ate/urdf/motion_analysis.py +396 -0
- ate/urdf/pipeline.py +468 -0
- ate/urdf/scale.py +256 -0
- ate/urdf/scan_session.py +411 -0
- ate/urdf/segmentation.py +299 -0
- ate/urdf/synthesis.py +319 -0
- ate/urdf/topology.py +336 -0
- ate/urdf/validation.py +371 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +1 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.8.dist-info/RECORD +0 -73
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {foodforthought_cli-0.2.8.dist-info → foodforthought_cli-0.3.0.dist-info}/top_level.txt +0 -0
ate/commands/parts.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parts catalog commands for FoodforThought CLI.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- ate parts list - List available parts
|
|
6
|
+
- ate parts check - Check part compatibility for skill
|
|
7
|
+
- ate parts require - Add part dependency to skill
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parts_list(client, category: Optional[str], manufacturer: Optional[str],
|
|
15
|
+
search: Optional[str]) -> None:
|
|
16
|
+
"""List available parts."""
|
|
17
|
+
print("Fetching parts catalog...")
|
|
18
|
+
|
|
19
|
+
params = {}
|
|
20
|
+
if category:
|
|
21
|
+
params["category"] = category
|
|
22
|
+
print(f" Category: {category}")
|
|
23
|
+
if manufacturer:
|
|
24
|
+
params["manufacturer"] = manufacturer
|
|
25
|
+
print(f" Manufacturer: {manufacturer}")
|
|
26
|
+
if search:
|
|
27
|
+
params["search"] = search
|
|
28
|
+
print(f" Search: {search}")
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
response = client._request("GET", "/parts", params=params)
|
|
32
|
+
parts = response.get("parts", [])
|
|
33
|
+
pagination = response.get("pagination", {})
|
|
34
|
+
|
|
35
|
+
if not parts:
|
|
36
|
+
print("\nNo parts found matching criteria.")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
print(f"\n{'=' * 70}")
|
|
40
|
+
print(f"{'Part Name':<30} {'Category':<15} {'Manufacturer':<20}")
|
|
41
|
+
print(f"{'=' * 70}")
|
|
42
|
+
|
|
43
|
+
for part in parts:
|
|
44
|
+
name = part.get("name", "")[:28]
|
|
45
|
+
cat = part.get("category", "")[:13]
|
|
46
|
+
mfr = part.get("manufacturer", "")[:18]
|
|
47
|
+
print(f"{name:<30} {cat:<15} {mfr:<20}")
|
|
48
|
+
|
|
49
|
+
total = pagination.get("total", len(parts))
|
|
50
|
+
print(f"{'=' * 70}")
|
|
51
|
+
print(f"Showing {len(parts)} of {total} parts")
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"\n✗ Failed to list parts: {e}", file=sys.stderr)
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def parts_check(client, skill_id: str) -> None:
|
|
59
|
+
"""Check part compatibility for a skill."""
|
|
60
|
+
print(f"Checking parts for skill: {skill_id}")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
response = client._request("GET", f"/skills/{skill_id}/parts")
|
|
64
|
+
|
|
65
|
+
skill = response.get("skill", {})
|
|
66
|
+
parts = response.get("parts", [])
|
|
67
|
+
summary = response.get("summary", {})
|
|
68
|
+
by_category = response.get("byCategory", {})
|
|
69
|
+
|
|
70
|
+
print(f"\nSkill: {skill.get('name', skill_id)}")
|
|
71
|
+
print(f"Type: {skill.get('type', 'unknown')}")
|
|
72
|
+
|
|
73
|
+
if not parts:
|
|
74
|
+
print("\n✓ No part dependencies declared for this skill.")
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
print(f"\n{'=' * 70}")
|
|
78
|
+
print(f"Part Dependencies ({summary.get('total', 0)} total)")
|
|
79
|
+
print(f"{'=' * 70}")
|
|
80
|
+
|
|
81
|
+
for category, cat_parts in by_category.items():
|
|
82
|
+
print(f"\n{category.upper()}:")
|
|
83
|
+
for p in cat_parts:
|
|
84
|
+
part = p.get("part", {})
|
|
85
|
+
required = "REQUIRED" if p.get("required") else "optional"
|
|
86
|
+
version = p.get("minVersion", "any")
|
|
87
|
+
if p.get("maxVersion"):
|
|
88
|
+
version += f" - {p['maxVersion']}"
|
|
89
|
+
|
|
90
|
+
icon = "●" if p.get("required") else "○"
|
|
91
|
+
print(f" {icon} {part.get('name'):<30} [{required}] v{version}")
|
|
92
|
+
|
|
93
|
+
print(f"\n{'=' * 70}")
|
|
94
|
+
print(f"Summary: {summary.get('required', 0)} required, {summary.get('optional', 0)} optional")
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print(f"\n✗ Failed to check parts: {e}", file=sys.stderr)
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parts_require(client, part_id: str, skill_id: str, version: str,
|
|
102
|
+
required: bool) -> None:
|
|
103
|
+
"""Add part dependency to skill."""
|
|
104
|
+
print(f"Adding part dependency...")
|
|
105
|
+
print(f" Part ID: {part_id}")
|
|
106
|
+
print(f" Skill ID: {skill_id}")
|
|
107
|
+
print(f" Min Version: {version}")
|
|
108
|
+
print(f" Required: {required}")
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
response = client._request("POST", f"/parts/{part_id}/compatibility", json={
|
|
112
|
+
"skillId": skill_id,
|
|
113
|
+
"minVersion": version,
|
|
114
|
+
"required": required,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
compat = response.get("compatibility", {})
|
|
118
|
+
print(f"\n✓ Part dependency added!")
|
|
119
|
+
print(f" Compatibility ID: {compat.get('id')}")
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"\n✗ Failed to add part dependency: {e}", file=sys.stderr)
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def register_parser(subparsers):
|
|
127
|
+
"""Register parts commands with argparse."""
|
|
128
|
+
parts_parser = subparsers.add_parser("parts", help="Manage hardware parts catalog")
|
|
129
|
+
parts_subparsers = parts_parser.add_subparsers(dest="parts_action", help="Parts action")
|
|
130
|
+
|
|
131
|
+
# parts list
|
|
132
|
+
parts_list_parser = parts_subparsers.add_parser("list", help="List available parts")
|
|
133
|
+
parts_list_parser.add_argument("-c", "--category",
|
|
134
|
+
choices=["gripper", "sensor", "actuator", "controller",
|
|
135
|
+
"end-effector", "camera", "lidar", "force-torque"],
|
|
136
|
+
help="Filter by category")
|
|
137
|
+
parts_list_parser.add_argument("-m", "--manufacturer", help="Filter by manufacturer")
|
|
138
|
+
parts_list_parser.add_argument("-s", "--search", help="Search by name or part number")
|
|
139
|
+
|
|
140
|
+
# parts check
|
|
141
|
+
parts_check_parser = parts_subparsers.add_parser("check",
|
|
142
|
+
help="Check part compatibility for skill")
|
|
143
|
+
parts_check_parser.add_argument("skill_id", help="Skill ID to check")
|
|
144
|
+
|
|
145
|
+
# parts require
|
|
146
|
+
parts_require_parser = parts_subparsers.add_parser("require",
|
|
147
|
+
help="Add part dependency to skill")
|
|
148
|
+
parts_require_parser.add_argument("part_id", help="Part ID to require")
|
|
149
|
+
parts_require_parser.add_argument("-s", "--skill", required=True, help="Skill ID")
|
|
150
|
+
parts_require_parser.add_argument("-v", "--version", default="1.0.0",
|
|
151
|
+
help="Minimum version (default: 1.0.0)")
|
|
152
|
+
parts_require_parser.add_argument("--required", action="store_true",
|
|
153
|
+
help="Mark as required (not optional)")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def handle(client, args):
|
|
157
|
+
"""Handle parts commands."""
|
|
158
|
+
if args.parts_action == "list":
|
|
159
|
+
parts_list(client, args.category, args.manufacturer, args.search)
|
|
160
|
+
elif args.parts_action == "check":
|
|
161
|
+
parts_check(client, args.skill_id)
|
|
162
|
+
elif args.parts_action == "require":
|
|
163
|
+
parts_require(client, args.part_id, args.skill, args.version, args.required)
|
|
164
|
+
else:
|
|
165
|
+
print("Usage: ate parts {list|check|require}")
|
|
166
|
+
sys.exit(1)
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Primitive skills commands for FoodforThought CLI.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- ate primitive list - List primitive skills
|
|
6
|
+
- ate primitive get - Get primitive details
|
|
7
|
+
- ate primitive test - Submit test result for primitive
|
|
8
|
+
- ate primitive init - Initialize primitive skill template
|
|
9
|
+
- ate primitive push - Publish primitive to FoodforThought
|
|
10
|
+
- ate primitive deps - Manage primitive dependencies
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def primitive_list(client, robot: Optional[str], category: Optional[str],
|
|
21
|
+
status: Optional[str], tested: bool) -> None:
|
|
22
|
+
"""List primitive skills."""
|
|
23
|
+
print("Fetching primitives...")
|
|
24
|
+
|
|
25
|
+
params = {}
|
|
26
|
+
if robot:
|
|
27
|
+
params["robot"] = robot
|
|
28
|
+
if category:
|
|
29
|
+
params["category"] = category
|
|
30
|
+
if status:
|
|
31
|
+
params["status"] = status
|
|
32
|
+
if tested:
|
|
33
|
+
params["status"] = "tested,verified"
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
response = client._request("GET", "/primitives", params=params)
|
|
37
|
+
primitives = response.get("primitives", [])
|
|
38
|
+
|
|
39
|
+
if not primitives:
|
|
40
|
+
print("\nNo primitives found.")
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
print(f"\n{'=' * 70}")
|
|
44
|
+
print(f"{'Name':<25} {'Category':<15} {'Status':<12} {'Reliability'}")
|
|
45
|
+
print(f"{'=' * 70}")
|
|
46
|
+
|
|
47
|
+
for prim in primitives:
|
|
48
|
+
name = prim.get("name", "")[:23]
|
|
49
|
+
cat = prim.get("category", "")[:13]
|
|
50
|
+
prim_status = prim.get("status", "")[:10]
|
|
51
|
+
reliability = prim.get("reliabilityScore", 0)
|
|
52
|
+
reliability_str = f"{reliability:.0%}" if reliability else "N/A"
|
|
53
|
+
print(f"{name:<25} {cat:<15} {prim_status:<12} {reliability_str}")
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print(f"\n✗ Failed to list primitives: {e}", file=sys.stderr)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def primitive_get(client, primitive_id: str) -> None:
|
|
61
|
+
"""Get detailed information about a primitive."""
|
|
62
|
+
try:
|
|
63
|
+
response = client._request("GET", f"/primitives/{primitive_id}")
|
|
64
|
+
prim = response.get("primitive", {})
|
|
65
|
+
|
|
66
|
+
print(f"\n{'=' * 60}")
|
|
67
|
+
print(f"Primitive: {prim.get('displayName') or prim.get('name')}")
|
|
68
|
+
print(f"{'=' * 60}")
|
|
69
|
+
|
|
70
|
+
print(f"\nCategory: {prim.get('category', '?')}")
|
|
71
|
+
print(f"Status: {prim.get('status', '?')}")
|
|
72
|
+
|
|
73
|
+
if prim.get('reliabilityScore'):
|
|
74
|
+
print(f"Reliability: {prim.get('reliabilityScore'):.1%}")
|
|
75
|
+
|
|
76
|
+
if prim.get('description'):
|
|
77
|
+
print(f"\nDescription: {prim.get('description')}")
|
|
78
|
+
|
|
79
|
+
print(f"\nCommand Type: {prim.get('commandType')}")
|
|
80
|
+
print(f"Command Template:")
|
|
81
|
+
print(f" {prim.get('commandTemplate')}")
|
|
82
|
+
|
|
83
|
+
# Parameters
|
|
84
|
+
params = prim.get('parameters', [])
|
|
85
|
+
if params:
|
|
86
|
+
print(f"\nParameters:")
|
|
87
|
+
for p in params:
|
|
88
|
+
range_str = ""
|
|
89
|
+
if p.get('min') is not None and p.get('max') is not None:
|
|
90
|
+
range_str = f" (range: {p.get('min')}-{p.get('max')})"
|
|
91
|
+
unit = f" {p.get('unit')}" if p.get('unit') else ""
|
|
92
|
+
default = f", default: {p.get('default')}" if p.get('default') is not None else ""
|
|
93
|
+
print(f" - {p.get('name')}: {p.get('type')}{range_str}{unit}{default}")
|
|
94
|
+
|
|
95
|
+
# Timing
|
|
96
|
+
if any([prim.get('executionTimeMs'), prim.get('settleTimeMs'), prim.get('cooldownMs')]):
|
|
97
|
+
print(f"\nTiming:")
|
|
98
|
+
if prim.get('executionTimeMs'):
|
|
99
|
+
print(f" Execution: {prim.get('executionTimeMs')}ms")
|
|
100
|
+
if prim.get('settleTimeMs'):
|
|
101
|
+
print(f" Settle: {prim.get('settleTimeMs')}ms")
|
|
102
|
+
if prim.get('cooldownMs'):
|
|
103
|
+
print(f" Cooldown: {prim.get('cooldownMs')}ms")
|
|
104
|
+
|
|
105
|
+
# Safety
|
|
106
|
+
if prim.get('safetyNotes'):
|
|
107
|
+
print(f"\nSafety Notes:")
|
|
108
|
+
print(f" {prim.get('safetyNotes')}")
|
|
109
|
+
|
|
110
|
+
except Exception as e:
|
|
111
|
+
print(f"\n✗ Failed to get primitive: {e}", file=sys.stderr)
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def primitive_test(client, primitive_id: str, params_json: str,
|
|
116
|
+
result: str, notes: Optional[str], video: Optional[str]) -> None:
|
|
117
|
+
"""Submit a test result for a primitive skill."""
|
|
118
|
+
try:
|
|
119
|
+
parameters = json.loads(params_json)
|
|
120
|
+
except json.JSONDecodeError as e:
|
|
121
|
+
print(f"Error: Invalid JSON for parameters: {e}", file=sys.stderr)
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
|
|
124
|
+
data = {
|
|
125
|
+
"parameters": parameters,
|
|
126
|
+
"result": result,
|
|
127
|
+
}
|
|
128
|
+
if notes:
|
|
129
|
+
data["resultNotes"] = notes
|
|
130
|
+
if video:
|
|
131
|
+
data["videoUrl"] = video
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
response = client._request("POST", f"/primitives/{primitive_id}/test", json=data)
|
|
135
|
+
update = response.get("primitiveUpdate", {})
|
|
136
|
+
|
|
137
|
+
print(f"\n✓ Test result submitted!")
|
|
138
|
+
print(f" Result: {result}")
|
|
139
|
+
print(f" New Reliability Score: {update.get('reliabilityScore', 0):.1%}")
|
|
140
|
+
|
|
141
|
+
if update.get('statusChanged'):
|
|
142
|
+
print(f" Status upgraded to: {update.get('status')}")
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
print(f"\n✗ Failed to submit test: {e}", file=sys.stderr)
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def primitive_init(client, name: str, protocol_id: Optional[str],
|
|
150
|
+
from_recording: Optional[str], category: str, output: str) -> None:
|
|
151
|
+
"""Initialize a new primitive skill definition locally."""
|
|
152
|
+
print(f"\n{'=' * 60}")
|
|
153
|
+
print("Initializing Primitive Skill")
|
|
154
|
+
print(f"{'=' * 60}\n")
|
|
155
|
+
|
|
156
|
+
out_path = Path(output)
|
|
157
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
|
|
159
|
+
primitive_data = {
|
|
160
|
+
"name": name,
|
|
161
|
+
"displayName": name.replace("_", " ").title(),
|
|
162
|
+
"category": category,
|
|
163
|
+
"description": "",
|
|
164
|
+
"protocolId": protocol_id,
|
|
165
|
+
"commandType": "single",
|
|
166
|
+
"commandTemplate": "",
|
|
167
|
+
"responsePattern": "",
|
|
168
|
+
"parameters": [],
|
|
169
|
+
"executionTimeMs": None,
|
|
170
|
+
"settleTimeMs": None,
|
|
171
|
+
"cooldownMs": None,
|
|
172
|
+
"safetyNotes": "",
|
|
173
|
+
"status": "experimental",
|
|
174
|
+
"version": "1.0.0",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# If importing from a recording, populate command data
|
|
178
|
+
if from_recording:
|
|
179
|
+
recording_path = Path(from_recording)
|
|
180
|
+
if recording_path.exists():
|
|
181
|
+
with open(recording_path) as f:
|
|
182
|
+
recording = json.load(f)
|
|
183
|
+
|
|
184
|
+
commands = recording.get("commands", [])
|
|
185
|
+
if commands:
|
|
186
|
+
primitive_data["commandTemplate"] = commands[0].get("command", "")
|
|
187
|
+
if len(commands) > 1:
|
|
188
|
+
primitive_data["commandType"] = "sequence"
|
|
189
|
+
primitive_data["commandSequence"] = [
|
|
190
|
+
{"command": c.get("command"), "delayMs": int((c.get("timestamp", 0) * 1000))}
|
|
191
|
+
for c in commands
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
responses = [c.get("response") for c in commands if c.get("response")]
|
|
195
|
+
if responses:
|
|
196
|
+
primitive_data["responsePattern"] = responses[0]
|
|
197
|
+
|
|
198
|
+
print(f"✓ Imported {len(commands)} commands from recording")
|
|
199
|
+
else:
|
|
200
|
+
print(f"Warning: Recording file not found: {from_recording}", file=sys.stderr)
|
|
201
|
+
|
|
202
|
+
# Write primitive file
|
|
203
|
+
primitive_file = out_path / f"{name}.primitive.json"
|
|
204
|
+
with open(primitive_file, "w") as f:
|
|
205
|
+
json.dump(primitive_data, f, indent=2)
|
|
206
|
+
|
|
207
|
+
print(f"✓ Created: {primitive_file}")
|
|
208
|
+
print(f"\nNext steps:")
|
|
209
|
+
print(f" 1. Edit {primitive_file} to define command template and parameters")
|
|
210
|
+
print(f" 2. Test with: ate primitive test <primitive_id> --params '{{...}}'")
|
|
211
|
+
print(f" 3. Publish with: ate primitive push {primitive_file}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def primitive_push(client, primitive_file: str) -> None:
|
|
215
|
+
"""Push a primitive skill definition to FoodforThought."""
|
|
216
|
+
file_path = Path(primitive_file)
|
|
217
|
+
if not file_path.exists():
|
|
218
|
+
print(f"Error: Primitive file not found: {primitive_file}", file=sys.stderr)
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
print(f"\n{'=' * 60}")
|
|
222
|
+
print("Publishing Primitive Skill")
|
|
223
|
+
print(f"{'=' * 60}\n")
|
|
224
|
+
|
|
225
|
+
with open(file_path) as f:
|
|
226
|
+
primitive_data = json.load(f)
|
|
227
|
+
|
|
228
|
+
print(f"Name: {primitive_data.get('displayName') or primitive_data.get('name')}")
|
|
229
|
+
print(f"Category: {primitive_data.get('category')}")
|
|
230
|
+
|
|
231
|
+
# Validate required fields
|
|
232
|
+
if not primitive_data.get("name"):
|
|
233
|
+
print("Error: Primitive must have a name", file=sys.stderr)
|
|
234
|
+
sys.exit(1)
|
|
235
|
+
|
|
236
|
+
if not primitive_data.get("commandTemplate") and not primitive_data.get("commandSequence"):
|
|
237
|
+
print("Error: Primitive must have a commandTemplate or commandSequence", file=sys.stderr)
|
|
238
|
+
sys.exit(1)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
response = client._request("POST", "/primitives", json=primitive_data)
|
|
242
|
+
prim = response.get("primitive", {})
|
|
243
|
+
print(f"\n✓ Primitive published successfully!")
|
|
244
|
+
print(f" ID: {prim.get('id')}")
|
|
245
|
+
print(f" Status: {prim.get('status')}")
|
|
246
|
+
except Exception as e:
|
|
247
|
+
print(f"\n✗ Failed to publish: {e}", file=sys.stderr)
|
|
248
|
+
sys.exit(1)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def primitive_deps_show(client, primitive_id: str) -> None:
|
|
252
|
+
"""Show dependencies for a primitive skill."""
|
|
253
|
+
try:
|
|
254
|
+
response = client._request("GET", f"/primitives/{primitive_id}/dependencies")
|
|
255
|
+
deps = response.get("dependencies", [])
|
|
256
|
+
dependents = response.get("dependents", [])
|
|
257
|
+
deployment_ready = response.get("deploymentReady", False)
|
|
258
|
+
|
|
259
|
+
print(f"\n{'=' * 60}")
|
|
260
|
+
print("Dependency Graph")
|
|
261
|
+
print(f"{'=' * 60}")
|
|
262
|
+
|
|
263
|
+
print(f"\nDeployment Ready: {'✓ Yes' if deployment_ready else '✗ No'}")
|
|
264
|
+
|
|
265
|
+
if deps:
|
|
266
|
+
print(f"\nDepends On ({len(deps)}):")
|
|
267
|
+
for dep in deps:
|
|
268
|
+
req = dep.get("requiredSkill", {})
|
|
269
|
+
status_ok = req.get("status") in ["tested", "verified"]
|
|
270
|
+
icon = "✓" if status_ok else "✗"
|
|
271
|
+
print(f" {icon} {req.get('name')} ({req.get('status')})")
|
|
272
|
+
else:
|
|
273
|
+
print(f"\nNo dependencies (this is a root primitive)")
|
|
274
|
+
|
|
275
|
+
if dependents:
|
|
276
|
+
print(f"\nRequired By ({len(dependents)}):")
|
|
277
|
+
for dep in dependents:
|
|
278
|
+
skill = dep.get("dependentSkill", {})
|
|
279
|
+
print(f" - {skill.get('name')}")
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
print(f"\n✗ Failed to fetch dependencies: {e}", file=sys.stderr)
|
|
283
|
+
sys.exit(1)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def primitive_deps_add(client, primitive_id: str, required_id: str,
|
|
287
|
+
dependency_type: str, min_status: str) -> None:
|
|
288
|
+
"""Add a dependency to a primitive skill."""
|
|
289
|
+
data = {
|
|
290
|
+
"requiredSkillId": required_id,
|
|
291
|
+
"dependencyType": dependency_type,
|
|
292
|
+
"requiredMinStatus": min_status,
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
response = client._request("POST", f"/primitives/{primitive_id}/dependencies", json=data)
|
|
297
|
+
cross_robot = response.get("crossRobot", False)
|
|
298
|
+
|
|
299
|
+
print(f"\n✓ Dependency added!")
|
|
300
|
+
if cross_robot:
|
|
301
|
+
print(f" ⚠ Note: This is a cross-robot dependency")
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
print(f"\n✗ Failed to add dependency: {e}", file=sys.stderr)
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def register_parser(subparsers):
|
|
309
|
+
"""Register primitive commands with argparse."""
|
|
310
|
+
primitive_parser = subparsers.add_parser("primitive", help="Manage primitive skills")
|
|
311
|
+
primitive_subparsers = primitive_parser.add_subparsers(dest="primitive_action", help="Primitive action")
|
|
312
|
+
|
|
313
|
+
# primitive list
|
|
314
|
+
primitive_list_parser = primitive_subparsers.add_parser("list", help="List primitive skills")
|
|
315
|
+
primitive_list_parser.add_argument("-r", "--robot", help="Filter by robot model")
|
|
316
|
+
primitive_list_parser.add_argument("-c", "--category",
|
|
317
|
+
choices=["body_pose", "arm", "gripper", "locomotion",
|
|
318
|
+
"head", "sensing", "manipulation", "navigation"],
|
|
319
|
+
help="Filter by category")
|
|
320
|
+
primitive_list_parser.add_argument("--status",
|
|
321
|
+
choices=["experimental", "tested", "verified", "deprecated"],
|
|
322
|
+
help="Filter by status")
|
|
323
|
+
primitive_list_parser.add_argument("--tested", action="store_true",
|
|
324
|
+
help="Show only tested/verified primitives")
|
|
325
|
+
|
|
326
|
+
# primitive get
|
|
327
|
+
primitive_get_parser = primitive_subparsers.add_parser("get", help="Get primitive details")
|
|
328
|
+
primitive_get_parser.add_argument("primitive_id", help="Primitive ID")
|
|
329
|
+
|
|
330
|
+
# primitive test
|
|
331
|
+
primitive_test_parser = primitive_subparsers.add_parser("test", help="Submit test result")
|
|
332
|
+
primitive_test_parser.add_argument("primitive_id", help="Primitive ID to test")
|
|
333
|
+
primitive_test_parser.add_argument("-p", "--params", required=True,
|
|
334
|
+
help="Parameters used in test as JSON")
|
|
335
|
+
primitive_test_parser.add_argument("-r", "--result", required=True,
|
|
336
|
+
choices=["pass", "fail", "partial"],
|
|
337
|
+
help="Test result")
|
|
338
|
+
primitive_test_parser.add_argument("-n", "--notes", help="Test notes")
|
|
339
|
+
primitive_test_parser.add_argument("-v", "--video", help="Video URL of test")
|
|
340
|
+
|
|
341
|
+
# primitive deps
|
|
342
|
+
primitive_deps_parser = primitive_subparsers.add_parser("deps", help="Manage primitive dependencies")
|
|
343
|
+
primitive_deps_subparsers = primitive_deps_parser.add_subparsers(dest="primitive_deps_action",
|
|
344
|
+
help="Dependency action")
|
|
345
|
+
|
|
346
|
+
# primitive deps show
|
|
347
|
+
primitive_deps_show_parser = primitive_deps_subparsers.add_parser("show", help="Show dependencies")
|
|
348
|
+
primitive_deps_show_parser.add_argument("primitive_id", help="Primitive ID")
|
|
349
|
+
|
|
350
|
+
# primitive deps add
|
|
351
|
+
primitive_deps_add_parser = primitive_deps_subparsers.add_parser("add", help="Add dependency")
|
|
352
|
+
primitive_deps_add_parser.add_argument("primitive_id", help="Primitive ID (the one that depends)")
|
|
353
|
+
primitive_deps_add_parser.add_argument("required_id", help="Required primitive ID")
|
|
354
|
+
primitive_deps_add_parser.add_argument("-t", "--type", default="requires",
|
|
355
|
+
choices=["requires", "extends", "overrides", "optional"],
|
|
356
|
+
help="Dependency type (default: requires)")
|
|
357
|
+
primitive_deps_add_parser.add_argument("--min-status", default="tested",
|
|
358
|
+
choices=["experimental", "tested", "verified"],
|
|
359
|
+
help="Minimum required status (default: tested)")
|
|
360
|
+
|
|
361
|
+
# primitive init
|
|
362
|
+
primitive_init_parser = primitive_subparsers.add_parser("init", help="Initialize primitive skill template")
|
|
363
|
+
primitive_init_parser.add_argument("name", help="Primitive name (e.g., move_joint, grip_close)")
|
|
364
|
+
primitive_init_parser.add_argument("-p", "--protocol", help="Protocol ID to link to")
|
|
365
|
+
primitive_init_parser.add_argument("-r", "--from-recording", help="Import from recording file")
|
|
366
|
+
primitive_init_parser.add_argument("-c", "--category", default="motion",
|
|
367
|
+
choices=["body_pose", "arm", "gripper", "locomotion",
|
|
368
|
+
"head", "sensing", "manipulation", "navigation"],
|
|
369
|
+
help="Primitive category (default: motion)")
|
|
370
|
+
primitive_init_parser.add_argument("-o", "--output", default=".", help="Output directory")
|
|
371
|
+
|
|
372
|
+
# primitive push
|
|
373
|
+
primitive_push_parser = primitive_subparsers.add_parser("push", help="Publish primitive to FoodforThought")
|
|
374
|
+
primitive_push_parser.add_argument("primitive_file", help="Path to .primitive.json file")
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def handle(client, args):
|
|
378
|
+
"""Handle primitive commands."""
|
|
379
|
+
if args.primitive_action == "list":
|
|
380
|
+
primitive_list(client, args.robot, args.category, args.status, args.tested)
|
|
381
|
+
elif args.primitive_action == "get":
|
|
382
|
+
primitive_get(client, args.primitive_id)
|
|
383
|
+
elif args.primitive_action == "test":
|
|
384
|
+
primitive_test(client, args.primitive_id, args.params, args.result, args.notes, args.video)
|
|
385
|
+
elif args.primitive_action == "init":
|
|
386
|
+
primitive_init(client, args.name, args.protocol, args.from_recording, args.category, args.output)
|
|
387
|
+
elif args.primitive_action == "push":
|
|
388
|
+
primitive_push(client, args.primitive_file)
|
|
389
|
+
elif args.primitive_action == "deps":
|
|
390
|
+
if args.primitive_deps_action == "show":
|
|
391
|
+
primitive_deps_show(client, args.primitive_id)
|
|
392
|
+
elif args.primitive_deps_action == "add":
|
|
393
|
+
primitive_deps_add(client, args.primitive_id, args.required_id, args.type, args.min_status)
|
|
394
|
+
else:
|
|
395
|
+
print("Usage: ate primitive deps {show|add}")
|
|
396
|
+
sys.exit(1)
|
|
397
|
+
else:
|
|
398
|
+
print("Usage: ate primitive {list|get|test|init|push|deps}")
|
|
399
|
+
sys.exit(1)
|