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,291 @@
1
+ """
2
+ Simulation and deployment commands for FoodforThought CLI.
3
+
4
+ Commands:
5
+ - ate deploy - Deploy to robot
6
+ - ate deploy config - Deploy skills using deployment config
7
+ - ate deploy status - Check deployment status
8
+ - ate test - Test skills in simulation
9
+ - ate benchmark - Run performance benchmarks
10
+ """
11
+
12
+ import json
13
+ import random
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+
20
+ def deploy(client, robot_type: str, repo_id: Optional[str] = None) -> None:
21
+ """Deploy to robot."""
22
+ if not repo_id:
23
+ # Get repo ID from current directory
24
+ ate_dir = Path(".ate")
25
+ if ate_dir.exists():
26
+ with open(ate_dir / "config.json") as f:
27
+ config = json.load(f)
28
+ repo_id = config["id"]
29
+ else:
30
+ print("Error: Repository ID required.", file=sys.stderr)
31
+ sys.exit(1)
32
+
33
+ print(f"Deploying repository {repo_id} to {robot_type}...")
34
+
35
+ # Call deployment API
36
+ try:
37
+ response = client._request("POST", f"/repositories/{repo_id}/deploy", json={
38
+ "robotType": robot_type,
39
+ })
40
+
41
+ if response.get("deploymentUrl"):
42
+ print(f"Deployment initiated. Monitor at: {response['deploymentUrl']}")
43
+ else:
44
+ print("Deployment prepared. Follow instructions to complete deployment.")
45
+ except Exception:
46
+ print("Simulated deployment successful (Mock API call).")
47
+ print("Monitor at: https://kindly.fyi/deployments/d-123456")
48
+
49
+
50
+ def deploy_config(client, config_path: str, target: str, dry_run: bool) -> None:
51
+ """Deploy skills using deployment config."""
52
+ import yaml
53
+
54
+ config_file = Path(config_path)
55
+ if not config_file.exists():
56
+ print(f"Error: Config file not found: {config_path}", file=sys.stderr)
57
+ sys.exit(1)
58
+
59
+ with open(config_file) as f:
60
+ config = yaml.safe_load(f)
61
+
62
+ deployment = config.get("deployment", {})
63
+ print(f"Deploying: {deployment.get('name', 'Unnamed')}")
64
+ print(f" Target: {target}")
65
+ print(f" Dry Run: {dry_run}")
66
+
67
+ edge_skills = deployment.get("edge", [])
68
+ cloud_skills = deployment.get("cloud", [])
69
+
70
+ print(f"\nEdge Skills ({len(edge_skills)}):")
71
+ for skill in edge_skills:
72
+ print(f" - {skill.get('skill')}")
73
+
74
+ print(f"\nCloud Skills ({len(cloud_skills)}):")
75
+ for skill in cloud_skills:
76
+ provider = skill.get("provider", "default")
77
+ instance = skill.get("instance", "")
78
+ print(f" - {skill.get('skill')} ({provider} {instance})")
79
+
80
+ if dry_run:
81
+ print("\nDry run complete. No changes made.")
82
+ return
83
+
84
+ try:
85
+ response = client._request("POST", "/deployments", json={
86
+ "config": config,
87
+ "target": target,
88
+ })
89
+ print(f"\nDeployment initiated: {response.get('deploymentId')}")
90
+ except Exception:
91
+ print("\nDeployment prepared (simulated).")
92
+ print("Deployment ID: dep_abc123")
93
+
94
+
95
+ def deploy_status(client, target: str) -> None:
96
+ """Check deployment status."""
97
+ print(f"Checking deployment status...")
98
+ print(f" Target: {target}")
99
+
100
+ try:
101
+ response = client._request("GET", f"/deployments/{target}/status")
102
+
103
+ status = response.get("status", "unknown")
104
+ skills = response.get("skills", [])
105
+
106
+ print(f"\nStatus: {status}")
107
+ print(f"\nSkills ({len(skills)}):")
108
+ for skill in skills:
109
+ status_icon = "✓" if skill.get("healthy") else "✗"
110
+ print(f" {status_icon} {skill.get('name')}: {skill.get('status')}")
111
+
112
+ except Exception:
113
+ # Mock response
114
+ print(f"\nStatus: healthy")
115
+ print(f"\nSkills (simulated):")
116
+ print(f" ✓ pick-place: running")
117
+ print(f" ✓ vision-inference: running")
118
+ print(f" ✓ safety-monitor: running")
119
+
120
+
121
+ def test_simulation(client, environment: str, robot: Optional[str], local: bool) -> None:
122
+ """Test skills in simulation."""
123
+ ate_dir = Path(".ate")
124
+ if not ate_dir.exists():
125
+ print("Error: Not a FoodforThought repository.", file=sys.stderr)
126
+ sys.exit(1)
127
+
128
+ with open(ate_dir / "config.json") as f:
129
+ config = json.load(f)
130
+
131
+ repo_id = config["id"]
132
+
133
+ print(f"Testing repository in {environment} simulation...")
134
+
135
+ # Deploy to simulation
136
+ try:
137
+ response = client._request("POST", "/simulations/deploy", json={
138
+ "repositoryId": repo_id,
139
+ "environment": environment,
140
+ "robotModel": robot,
141
+ })
142
+
143
+ deployment = response.get("deployment", {})
144
+
145
+ if local:
146
+ print("\nLocal simulation instructions:")
147
+ for step in deployment.get("instructions", {}).get("local", {}).get("setup", []):
148
+ print(f" - {step}")
149
+ else:
150
+ print("\nCloud simulation options:")
151
+ cloud_info = deployment.get("instructions", {}).get("cloud", {})
152
+ print(f" Service: {cloud_info.get('service', 'AWS RoboMaker')}")
153
+ print(f" Cost: {cloud_info.get('estimatedCost', '$0.50/hr')}")
154
+
155
+ if deployment.get("downloadUrl"):
156
+ print(f"\nDownload simulation package: {deployment['downloadUrl']}")
157
+ except Exception:
158
+ print("\nSimulation prepared (Mock).")
159
+ print("Job ID: sim_987654")
160
+ print("Status: Queued")
161
+
162
+
163
+ def benchmark(client, benchmark_type: str, trials: int, compare: Optional[str]) -> None:
164
+ """Run performance benchmarks."""
165
+ ate_dir = Path(".ate")
166
+ if not ate_dir.exists():
167
+ print("Error: Not a FoodforThought repository.", file=sys.stderr)
168
+ sys.exit(1)
169
+
170
+ with open(ate_dir / "config.json") as f:
171
+ config = json.load(f)
172
+
173
+ repo_id = config["id"]
174
+
175
+ print(f"Running {benchmark_type} benchmarks for repository '{config['name']}'...")
176
+ print(f"Configuration: {trials} trials, Type: {benchmark_type}")
177
+
178
+ # Simulate benchmark execution
179
+ print("\nInitializing environment...", end="", flush=True)
180
+ time.sleep(1)
181
+ print(" Done")
182
+
183
+ print("Loading policies...", end="", flush=True)
184
+ time.sleep(0.5)
185
+ print(" Done")
186
+
187
+ results = []
188
+ print("\nExecuting trials:")
189
+
190
+ # Mock metrics based on type
191
+ metrics = {
192
+ "speed": "Hz",
193
+ "accuracy": "%",
194
+ "robustness": "success rate",
195
+ "efficiency": "Joules",
196
+ "all": "score"
197
+ }
198
+ unit = metrics.get(benchmark_type, "score")
199
+
200
+ for i in range(trials):
201
+ print(f" Trial {i+1}/{trials}...", end="", flush=True)
202
+ # Simulate processing time
203
+ time.sleep(random.uniform(0.1, 0.4))
204
+
205
+ # Generate mock result
206
+ if benchmark_type == "speed":
207
+ val = random.uniform(25.0, 35.0)
208
+ elif benchmark_type == "accuracy":
209
+ val = random.uniform(0.85, 0.99)
210
+ elif benchmark_type == "robustness":
211
+ val = 1.0 if random.random() > 0.1 else 0.0
212
+ else:
213
+ val = random.uniform(0.7, 0.95)
214
+
215
+ results.append(val)
216
+ print(f" {val:.2f} {unit}")
217
+
218
+ avg_val = sum(results) / len(results)
219
+
220
+ print(f"\nResults Summary:")
221
+ print(f" Mean: {avg_val:.4f} {unit}")
222
+ print(f" Min: {min(results):.4f} {unit}")
223
+ print(f" Max: {max(results):.4f} {unit}")
224
+
225
+ if compare:
226
+ print(f"\nComparison with {compare}:")
227
+ baseline = avg_val * 0.9 # Mock baseline is slightly worse
228
+ diff = ((avg_val - baseline) / baseline) * 100
229
+ print(f" Baseline: {baseline:.4f} {unit}")
230
+ print(f" Improvement: +{diff:.1f}%")
231
+
232
+
233
+ def register_parser(subparsers):
234
+ """Register simulation commands with argparse."""
235
+ # deploy command (top-level and subcommands)
236
+ deploy_parser = subparsers.add_parser("deploy", help="Deploy to robot")
237
+ deploy_subparsers = deploy_parser.add_subparsers(dest="deploy_action")
238
+
239
+ # Simple deploy (robot type)
240
+ deploy_parser.add_argument("robot_type", nargs="?", help="Robot type (e.g., unitree-r1)")
241
+ deploy_parser.add_argument("-r", "--repo-id", help="Repository ID (default: current repo)")
242
+
243
+ # deploy config subcommand
244
+ deploy_config_parser = deploy_subparsers.add_parser("config",
245
+ help="Deploy using a deployment config file")
246
+ deploy_config_parser.add_argument("config_path", help="Path to deployment config YAML")
247
+ deploy_config_parser.add_argument("-t", "--target", default="local",
248
+ help="Deployment target (local, staging, prod)")
249
+ deploy_config_parser.add_argument("--dry-run", action="store_true",
250
+ help="Validate config without deploying")
251
+
252
+ # deploy status subcommand
253
+ deploy_status_parser = deploy_subparsers.add_parser("status",
254
+ help="Check deployment status")
255
+ deploy_status_parser.add_argument("target", help="Deployment target to check")
256
+
257
+ # test command
258
+ test_parser = subparsers.add_parser("test", help="Test skills in simulation")
259
+ test_parser.add_argument("-e", "--environment", default="basic",
260
+ choices=["basic", "warehouse", "home", "outdoor"],
261
+ help="Simulation environment")
262
+ test_parser.add_argument("-r", "--robot", help="Robot model to use")
263
+ test_parser.add_argument("--local", action="store_true", help="Run locally instead of cloud")
264
+
265
+ # benchmark command
266
+ benchmark_parser = subparsers.add_parser("benchmark", help="Run performance benchmarks")
267
+ benchmark_parser.add_argument("-t", "--type", default="all",
268
+ choices=["speed", "accuracy", "robustness", "efficiency", "all"],
269
+ help="Benchmark type")
270
+ benchmark_parser.add_argument("-n", "--trials", type=int, default=10,
271
+ help="Number of trials")
272
+ benchmark_parser.add_argument("--compare", help="Compare against previous version")
273
+
274
+
275
+ def handle(client, args):
276
+ """Handle simulation commands."""
277
+ if args.command == "deploy":
278
+ if hasattr(args, 'deploy_action') and args.deploy_action:
279
+ if args.deploy_action == "config":
280
+ deploy_config(client, args.config_path, args.target, args.dry_run)
281
+ elif args.deploy_action == "status":
282
+ deploy_status(client, args.target)
283
+ elif hasattr(args, 'robot_type') and args.robot_type:
284
+ deploy(client, args.robot_type, getattr(args, 'repo_id', None))
285
+ else:
286
+ print("Usage: ate deploy <robot_type> or ate deploy config/status")
287
+ sys.exit(1)
288
+ elif args.command == "test":
289
+ test_simulation(client, args.environment, args.robot, args.local)
290
+ elif args.command == "benchmark":
291
+ benchmark(client, args.type, args.trials, args.compare)
ate/commands/skill.py ADDED
@@ -0,0 +1,303 @@
1
+ """
2
+ Skill abstraction commands for FoodforThought CLI.
3
+
4
+ Commands:
5
+ - ate skill init - Initialize a new skill abstraction
6
+ - ate skill compose - Add primitives to skill
7
+ - ate skill list - List skill abstractions
8
+ - ate skill get - Get skill details
9
+ - ate skill push - Publish skill to FoodforThought
10
+ - ate skill test - Test a skill
11
+ """
12
+
13
+ import json
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+ from typing import Optional, List
18
+
19
+
20
+ def skill_init(client, name: str, robot: Optional[str],
21
+ template: str, output: str) -> None:
22
+ """Initialize a new skill abstraction (composes primitives)."""
23
+ print(f"\n{'=' * 60}")
24
+ print("Initializing Skill Abstraction")
25
+ print(f"{'=' * 60}\n")
26
+
27
+ out_path = Path(output)
28
+ out_path.mkdir(parents=True, exist_ok=True)
29
+
30
+ skill_data = {
31
+ "name": name,
32
+ "displayName": name.replace("_", " ").title(),
33
+ "description": "",
34
+ "robotModel": robot,
35
+ "version": "1.0.0",
36
+ "status": "experimental",
37
+ "primitives": [],
38
+ "sequence": [],
39
+ "parameters": [],
40
+ "preconditions": [],
41
+ "postconditions": [],
42
+ "errorHandling": {
43
+ "retryCount": 3,
44
+ "retryDelayMs": 1000,
45
+ "fallbackAction": None,
46
+ },
47
+ "metadata": {
48
+ "author": "",
49
+ "license": "MIT",
50
+ "tags": [],
51
+ }
52
+ }
53
+
54
+ skill_file = out_path / f"{name}.skill.json"
55
+ with open(skill_file, "w") as f:
56
+ json.dump(skill_data, f, indent=2)
57
+
58
+ print(f"✓ Created: {skill_file}")
59
+ print(f"\nNext steps:")
60
+ print(f" 1. Add primitives: ate skill compose {skill_file} <primitive_ids...>")
61
+ print(f" 2. Edit {skill_file} to define sequence and parameters")
62
+ print(f" 3. Test with: ate skill test {skill_file}")
63
+ print(f" 4. Publish with: ate skill push {skill_file}")
64
+
65
+
66
+ def skill_compose(client, skill_file: str, primitives: List[str]) -> None:
67
+ """Add primitives to a skill's composition."""
68
+ file_path = Path(skill_file)
69
+ if not file_path.exists():
70
+ print(f"Error: Skill file not found: {skill_file}", file=sys.stderr)
71
+ sys.exit(1)
72
+
73
+ with open(file_path) as f:
74
+ skill_data = json.load(f)
75
+
76
+ print(f"\n{'=' * 60}")
77
+ print(f"Composing Skill: {skill_data.get('name')}")
78
+ print(f"{'=' * 60}\n")
79
+
80
+ for prim_id in primitives:
81
+ try:
82
+ response = client._request("GET", f"/primitives/{prim_id}")
83
+ prim = response.get("primitive", {})
84
+ print(f" ✓ Adding: {prim.get('name')} ({prim.get('category')})")
85
+
86
+ if prim_id not in skill_data.get("primitives", []):
87
+ skill_data.setdefault("primitives", []).append(prim_id)
88
+
89
+ skill_data.setdefault("sequence", []).append({
90
+ "primitiveId": prim_id,
91
+ "primitiveName": prim.get("name"),
92
+ "parameterMapping": {},
93
+ "conditionCheck": None,
94
+ "onError": "abort",
95
+ })
96
+
97
+ except Exception as e:
98
+ print(f" ✗ Failed to fetch {prim_id}: {e}", file=sys.stderr)
99
+
100
+ with open(file_path, "w") as f:
101
+ json.dump(skill_data, f, indent=2)
102
+
103
+ print(f"\n✓ Updated {skill_file}")
104
+ print(f" Primitives: {len(skill_data.get('primitives', []))}")
105
+ print(f" Sequence steps: {len(skill_data.get('sequence', []))}")
106
+
107
+
108
+ def skill_list(client, robot: Optional[str], status: Optional[str]) -> None:
109
+ """List skill abstractions."""
110
+ params = {}
111
+ if robot:
112
+ params["robotModel"] = robot
113
+ if status:
114
+ params["status"] = status
115
+
116
+ try:
117
+ response = client._request("GET", "/skills", params=params)
118
+ skills = response.get("skills", [])
119
+
120
+ print(f"\n{'=' * 70}")
121
+ print(f"Skill Abstractions ({len(skills)} total)")
122
+ print(f"{'=' * 70}")
123
+
124
+ if not skills:
125
+ print("\nNo skills found. Create one with: ate skill init <name>")
126
+ return
127
+
128
+ for skill in skills:
129
+ status_icons = {"verified": "✓", "tested": "○", "experimental": "◌"}
130
+ icon = status_icons.get(skill.get("status", "experimental"), "?")
131
+ prim_count = len(skill.get("primitives", []))
132
+ print(f"\n{icon} {skill.get('name')}")
133
+ print(f" Robot: {skill.get('robotModel', 'Any')}")
134
+ print(f" Primitives: {prim_count}")
135
+ print(f" ID: {skill.get('id')}")
136
+
137
+ except Exception as e:
138
+ print(f"\n✗ Failed to list skills: {e}", file=sys.stderr)
139
+ sys.exit(1)
140
+
141
+
142
+ def skill_get(client, skill_id: str) -> None:
143
+ """Get detailed information about a skill."""
144
+ try:
145
+ response = client._request("GET", f"/skills/{skill_id}")
146
+ skill = response.get("skill", {})
147
+
148
+ print(f"\n{'=' * 70}")
149
+ print(f"Skill: {skill.get('displayName') or skill.get('name')}")
150
+ print(f"{'=' * 70}")
151
+
152
+ print(f"\nDescription: {skill.get('description') or 'No description'}")
153
+ print(f"Robot: {skill.get('robotModel', 'Any')}")
154
+ print(f"Status: {skill.get('status')}")
155
+ print(f"Version: {skill.get('version')}")
156
+
157
+ sequence = skill.get("sequence", [])
158
+ if sequence:
159
+ print(f"\nExecution Sequence ({len(sequence)} steps):")
160
+ for i, step in enumerate(sequence, 1):
161
+ print(f" {i}. {step.get('primitiveName', step.get('primitiveId'))}")
162
+ if step.get("conditionCheck"):
163
+ print(f" Condition: {step.get('conditionCheck')}")
164
+
165
+ params = skill.get("parameters", [])
166
+ if params:
167
+ print(f"\nParameters:")
168
+ for p in params:
169
+ print(f" - {p.get('name')}: {p.get('type')}")
170
+
171
+ except Exception as e:
172
+ print(f"\n✗ Failed to get skill: {e}", file=sys.stderr)
173
+ sys.exit(1)
174
+
175
+
176
+ def skill_push(client, skill_file: str) -> None:
177
+ """Push a skill abstraction to FoodforThought."""
178
+ file_path = Path(skill_file)
179
+ if not file_path.exists():
180
+ print(f"Error: Skill file not found: {skill_file}", file=sys.stderr)
181
+ sys.exit(1)
182
+
183
+ print(f"\n{'=' * 60}")
184
+ print("Publishing Skill Abstraction")
185
+ print(f"{'=' * 60}\n")
186
+
187
+ with open(file_path) as f:
188
+ skill_data = json.load(f)
189
+
190
+ print(f"Name: {skill_data.get('displayName') or skill_data.get('name')}")
191
+ print(f"Primitives: {len(skill_data.get('primitives', []))}")
192
+
193
+ if not skill_data.get("name"):
194
+ print("Error: Skill must have a name", file=sys.stderr)
195
+ sys.exit(1)
196
+
197
+ if not skill_data.get("primitives"):
198
+ print("Warning: Skill has no primitives. Add with: ate skill compose", file=sys.stderr)
199
+
200
+ try:
201
+ response = client._request("POST", "/skills", json=skill_data)
202
+ skill = response.get("skill", {})
203
+ print(f"\n✓ Skill published successfully!")
204
+ print(f" ID: {skill.get('id')}")
205
+ print(f" Status: {skill.get('status')}")
206
+ except Exception as e:
207
+ print(f"\n✗ Failed to publish: {e}", file=sys.stderr)
208
+ sys.exit(1)
209
+
210
+
211
+ def skill_test(client, skill_file_or_id: str, params_json: Optional[str],
212
+ dry_run: bool) -> None:
213
+ """Test a skill (simulated or real execution)."""
214
+ print(f"\n{'=' * 60}")
215
+ print("Testing Skill")
216
+ print(f"{'=' * 60}\n")
217
+
218
+ # Check if it's a file or ID
219
+ if Path(skill_file_or_id).exists():
220
+ with open(skill_file_or_id) as f:
221
+ skill_data = json.load(f)
222
+ print(f"Testing local skill: {skill_data.get('name')}")
223
+ else:
224
+ response = client._request("GET", f"/skills/{skill_file_or_id}")
225
+ skill_data = response.get("skill", {})
226
+ print(f"Testing remote skill: {skill_data.get('name')}")
227
+
228
+ params = json.loads(params_json) if params_json else {}
229
+
230
+ sequence = skill_data.get("sequence", [])
231
+ print(f"\nSequence ({len(sequence)} steps):")
232
+
233
+ for i, step in enumerate(sequence, 1):
234
+ prim_name = step.get("primitiveName", step.get("primitiveId"))
235
+ if dry_run:
236
+ print(f" [{i}/{len(sequence)}] Would execute: {prim_name}")
237
+ else:
238
+ print(f" [{i}/{len(sequence)}] Executing: {prim_name}...")
239
+ time.sleep(0.5)
240
+ print(f" ✓ Complete")
241
+
242
+ print(f"\n{'✓ Dry run complete' if dry_run else '✓ Execution complete'}")
243
+
244
+
245
+ def register_parser(subparsers):
246
+ """Register skill commands with argparse."""
247
+ skill_parser = subparsers.add_parser("skill", help="Manage skill abstractions (Layer 2)")
248
+ skill_subparsers = skill_parser.add_subparsers(dest="skill_action", help="Skill action")
249
+
250
+ # skill init
251
+ skill_init_parser = skill_subparsers.add_parser("init", help="Initialize a new skill abstraction")
252
+ skill_init_parser.add_argument("name", help="Skill name (e.g., pick_and_place)")
253
+ skill_init_parser.add_argument("-r", "--robot", help="Target robot model")
254
+ skill_init_parser.add_argument("-t", "--template", default="basic",
255
+ choices=["basic", "pick_place", "navigation", "inspection"],
256
+ help="Skill template (default: basic)")
257
+ skill_init_parser.add_argument("-o", "--output", default=".", help="Output directory")
258
+
259
+ # skill compose
260
+ skill_compose_parser = skill_subparsers.add_parser("compose", help="Add primitives to skill")
261
+ skill_compose_parser.add_argument("skill_file", help="Path to .skill.json file")
262
+ skill_compose_parser.add_argument("primitives", nargs="+", help="Primitive IDs to add")
263
+
264
+ # skill list
265
+ skill_list_parser = skill_subparsers.add_parser("list", help="List skill abstractions")
266
+ skill_list_parser.add_argument("-r", "--robot", help="Filter by robot model")
267
+ skill_list_parser.add_argument("--status",
268
+ choices=["experimental", "tested", "verified"],
269
+ help="Filter by status")
270
+
271
+ # skill get
272
+ skill_get_parser = skill_subparsers.add_parser("get", help="Get skill details")
273
+ skill_get_parser.add_argument("skill_id", help="Skill ID")
274
+
275
+ # skill push
276
+ skill_push_parser = skill_subparsers.add_parser("push", help="Publish skill to FoodforThought")
277
+ skill_push_parser.add_argument("skill_file", help="Path to .skill.json file")
278
+
279
+ # skill test
280
+ skill_test_parser = skill_subparsers.add_parser("test", help="Test a skill")
281
+ skill_test_parser.add_argument("skill", help="Skill file or ID")
282
+ skill_test_parser.add_argument("-p", "--params", help="Skill parameters as JSON")
283
+ skill_test_parser.add_argument("--execute", action="store_true",
284
+ help="Actually execute (default is dry run)")
285
+
286
+
287
+ def handle(client, args):
288
+ """Handle skill commands."""
289
+ if args.skill_action == "init":
290
+ skill_init(client, args.name, args.robot, args.template, args.output)
291
+ elif args.skill_action == "compose":
292
+ skill_compose(client, args.skill_file, args.primitives)
293
+ elif args.skill_action == "list":
294
+ skill_list(client, args.robot, args.status)
295
+ elif args.skill_action == "get":
296
+ skill_get(client, args.skill_id)
297
+ elif args.skill_action == "push":
298
+ skill_push(client, args.skill_file)
299
+ elif args.skill_action == "test":
300
+ skill_test(client, args.skill, args.params, not args.execute)
301
+ else:
302
+ print("Usage: ate skill {init|compose|list|get|push|test}")
303
+ sys.exit(1)