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.
- 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 +100 -0
- ate/behaviors/approach.py +399 -0
- ate/behaviors/common.py +686 -0
- ate/behaviors/tree.py +454 -0
- ate/cli.py +855 -3995
- 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 +39 -0
- ate/drivers/ble_transport.py +405 -0
- ate/drivers/mechdog.py +942 -0
- ate/drivers/wifi_camera.py +477 -0
- ate/interfaces/__init__.py +187 -0
- ate/interfaces/base.py +273 -0
- ate/interfaces/body.py +267 -0
- ate/interfaces/detection.py +282 -0
- ate/interfaces/locomotion.py +422 -0
- ate/interfaces/manipulation.py +408 -0
- ate/interfaces/navigation.py +389 -0
- ate/interfaces/perception.py +362 -0
- ate/interfaces/sensors.py +247 -0
- ate/interfaces/types.py +371 -0
- ate/llm_proxy.py +239 -0
- ate/mcp_server.py +387 -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 +83 -0
- ate/recording/demonstration.py +378 -0
- ate/recording/session.py +415 -0
- ate/recording/upload.py +304 -0
- ate/recording/visual.py +416 -0
- ate/recording/wrapper.py +95 -0
- ate/robot/__init__.py +221 -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 +668 -0
- ate/robot/calibration_state.py +388 -0
- ate/robot/commands.py +3735 -0
- ate/robot/direction_calibration.py +554 -0
- ate/robot/discovery.py +441 -0
- ate/robot/introspection.py +330 -0
- ate/robot/llm_system_id.py +654 -0
- ate/robot/locomotion_calibration.py +508 -0
- ate/robot/manager.py +270 -0
- ate/robot/marker_generator.py +611 -0
- ate/robot/perception.py +502 -0
- ate/robot/primitives.py +614 -0
- ate/robot/profiles.py +281 -0
- ate/robot/registry.py +322 -0
- ate/robot/servo_mapper.py +1153 -0
- ate/robot/skill_upload.py +675 -0
- ate/robot/target_calibration.py +500 -0
- ate/robot/teach.py +515 -0
- ate/robot/types.py +242 -0
- ate/robot/visual_labeler.py +1048 -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.7.dist-info → foodforthought_cli-0.3.0.dist-info}/METADATA +9 -1
- foodforthought_cli-0.3.0.dist-info/RECORD +166 -0
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/WHEEL +1 -1
- foodforthought_cli-0.2.7.dist-info/RECORD +0 -44
- {foodforthought_cli-0.2.7.dist-info → foodforthought_cli-0.3.0.dist-info}/entry_points.txt +0 -0
- {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)
|