foodforthought-cli 0.1.1__py3-none-any.whl → 0.2.1__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/generator.py ADDED
@@ -0,0 +1,713 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Text-to-Skill Generator
4
+
5
+ Converts natural language task descriptions into skill scaffolding.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional, Tuple
13
+ from dataclasses import dataclass, field
14
+
15
+
16
+ @dataclass
17
+ class SkillTemplate:
18
+ """Represents a skill template type."""
19
+ name: str
20
+ keywords: List[str]
21
+ category: str
22
+ description: str
23
+ parameters: Dict[str, any] = field(default_factory=dict)
24
+
25
+
26
+ # Available skill templates
27
+ TEMPLATES = {
28
+ "pick_place": SkillTemplate(
29
+ name="pick_place",
30
+ keywords=["pick", "place", "grab", "grasp", "put", "move", "lift", "drop", "transfer"],
31
+ category="manipulation",
32
+ description="Pick and place manipulation skill",
33
+ parameters={
34
+ "approach_height": 0.1,
35
+ "grasp_depth": 0.02,
36
+ "place_height": 0.05,
37
+ "gripper_force": 10.0,
38
+ }
39
+ ),
40
+ "navigation": SkillTemplate(
41
+ name="navigation",
42
+ keywords=["navigate", "go", "move to", "drive", "path", "waypoint", "follow"],
43
+ category="navigation",
44
+ description="Mobile robot navigation skill",
45
+ parameters={
46
+ "max_velocity": 1.0,
47
+ "goal_tolerance": 0.1,
48
+ "obstacle_avoidance": True,
49
+ }
50
+ ),
51
+ "inspection": SkillTemplate(
52
+ name="inspection",
53
+ keywords=["inspect", "look", "check", "scan", "detect", "find", "locate", "vision"],
54
+ category="perception",
55
+ description="Visual inspection and detection skill",
56
+ parameters={
57
+ "detection_threshold": 0.8,
58
+ "camera_topic": "/camera/image_raw",
59
+ "model_type": "yolo",
60
+ }
61
+ ),
62
+ "assembly": SkillTemplate(
63
+ name="assembly",
64
+ keywords=["assemble", "connect", "attach", "insert", "join", "screw", "bolt"],
65
+ category="manipulation",
66
+ description="Assembly and insertion skill",
67
+ parameters={
68
+ "insertion_force": 5.0,
69
+ "alignment_tolerance": 0.001,
70
+ "compliance_mode": "force_feedback",
71
+ }
72
+ ),
73
+ "pouring": SkillTemplate(
74
+ name="pouring",
75
+ keywords=["pour", "fill", "empty", "transfer liquid", "container"],
76
+ category="manipulation",
77
+ description="Liquid pouring and transfer skill",
78
+ parameters={
79
+ "pour_angle": 45.0,
80
+ "pour_speed": 0.5,
81
+ "fill_level": 0.8,
82
+ }
83
+ ),
84
+ }
85
+
86
+
87
+ def parse_task_description(description: str) -> Tuple[str, Dict[str, str]]:
88
+ """
89
+ Parse a natural language task description to identify the skill type and parameters.
90
+
91
+ Returns:
92
+ Tuple of (template_name, extracted_params)
93
+ """
94
+ description_lower = description.lower()
95
+
96
+ # Score each template based on keyword matches
97
+ scores = {}
98
+ for name, template in TEMPLATES.items():
99
+ score = 0
100
+ for keyword in template.keywords:
101
+ if keyword in description_lower:
102
+ score += 1
103
+ # Bonus for exact word matches
104
+ if re.search(rf'\b{keyword}\b', description_lower):
105
+ score += 0.5
106
+ scores[name] = score
107
+
108
+ # Select template with highest score
109
+ best_template = max(scores, key=scores.get)
110
+
111
+ # If no keywords matched, default to pick_place
112
+ if scores[best_template] == 0:
113
+ best_template = "pick_place"
114
+
115
+ # Extract potential parameters from description
116
+ extracted_params = {}
117
+
118
+ # Try to extract object names
119
+ object_patterns = [
120
+ r'(?:pick up|grab|grasp|take|move|place|put)\s+(?:the\s+)?(\w+)',
121
+ r'(\w+)\s+(?:onto|on|to|into|in)\s+(?:the\s+)?(\w+)',
122
+ ]
123
+
124
+ for pattern in object_patterns:
125
+ match = re.search(pattern, description_lower)
126
+ if match:
127
+ if match.lastindex >= 1:
128
+ extracted_params["source_object"] = match.group(1)
129
+ if match.lastindex >= 2:
130
+ extracted_params["target_location"] = match.group(2)
131
+ break
132
+
133
+ return best_template, extracted_params
134
+
135
+
136
+ def generate_skill_yaml(template: SkillTemplate, task_description: str,
137
+ robot_model: str, extracted_params: Dict) -> str:
138
+ """Generate skill.yaml configuration file."""
139
+
140
+ # Create a clean skill name from description
141
+ skill_name = re.sub(r'[^\w\s]', '', task_description.lower())
142
+ skill_name = '_'.join(skill_name.split()[:4])
143
+
144
+ yaml_content = f"""# Skill Configuration
145
+ # Auto-generated from: "{task_description}"
146
+
147
+ name: "{skill_name}"
148
+ version: "1.0.0"
149
+ category: "{template.category}"
150
+ description: "{task_description}"
151
+
152
+ robot:
153
+ model: "{robot_model}"
154
+ required_components:
155
+ - gripper # TODO: Adjust based on task requirements
156
+ - camera # TODO: Add if perception needed
157
+
158
+ parameters:
159
+ """
160
+
161
+ # Add template parameters
162
+ for param, value in template.parameters.items():
163
+ if isinstance(value, str):
164
+ yaml_content += f' {param}: "{value}"\n'
165
+ elif isinstance(value, bool):
166
+ yaml_content += f' {param}: {"true" if value else "false"}\n'
167
+ else:
168
+ yaml_content += f' {param}: {value}\n'
169
+
170
+ # Add extracted parameters
171
+ if extracted_params:
172
+ yaml_content += "\n# Extracted from task description\n"
173
+ for param, value in extracted_params.items():
174
+ yaml_content += f' {param}: "{value}"\n'
175
+
176
+ yaml_content += """
177
+ # Safety constraints
178
+ safety:
179
+ max_velocity: 1.0 # m/s
180
+ max_force: 50.0 # N
181
+ workspace_bounds:
182
+ x: [-0.5, 0.5]
183
+ y: [-0.5, 0.5]
184
+ z: [0.0, 0.6]
185
+
186
+ # Dependencies (managed via `ate parts require`)
187
+ dependencies: []
188
+
189
+ # Metadata
190
+ metadata:
191
+ author: "" # TODO: Add your name
192
+ created: "" # Auto-filled on upload
193
+ tags:
194
+ - {template.category}
195
+ - {robot_model}
196
+ """
197
+
198
+ return yaml_content
199
+
200
+
201
+ def generate_main_py(template: SkillTemplate, task_description: str) -> str:
202
+ """Generate main.py implementation file."""
203
+
204
+ if template.name == "pick_place":
205
+ implementation = '''
206
+ def execute(self):
207
+ """Execute pick and place skill."""
208
+ # TODO: Implement pick and place logic
209
+
210
+ # Phase 1: Approach object
211
+ approach_pose = self.get_approach_pose()
212
+ self.move_to(approach_pose)
213
+
214
+ # Phase 2: Grasp object
215
+ grasp_pose = self.get_grasp_pose()
216
+ self.move_to(grasp_pose)
217
+ self.close_gripper()
218
+
219
+ # Phase 3: Lift and move
220
+ lift_pose = self.get_lift_pose()
221
+ self.move_to(lift_pose)
222
+
223
+ target_pose = self.get_target_pose()
224
+ self.move_to(target_pose)
225
+
226
+ # Phase 4: Place object
227
+ place_pose = self.get_place_pose()
228
+ self.move_to(place_pose)
229
+ self.open_gripper()
230
+
231
+ # Phase 5: Retract
232
+ retract_pose = self.get_retract_pose()
233
+ self.move_to(retract_pose)
234
+
235
+ return True
236
+ '''
237
+ elif template.name == "navigation":
238
+ implementation = '''
239
+ def execute(self):
240
+ """Execute navigation skill."""
241
+ # TODO: Implement navigation logic
242
+
243
+ # Get target waypoint
244
+ target = self.get_target_position()
245
+
246
+ # Plan path
247
+ path = self.plan_path(target)
248
+
249
+ # Follow path with obstacle avoidance
250
+ for waypoint in path:
251
+ self.move_to(waypoint)
252
+
253
+ if self.check_obstacles():
254
+ # Replan if obstacles detected
255
+ path = self.plan_path(target)
256
+
257
+ return self.at_goal(target)
258
+ '''
259
+ elif template.name == "inspection":
260
+ implementation = '''
261
+ def execute(self):
262
+ """Execute inspection skill."""
263
+ # TODO: Implement inspection logic
264
+
265
+ # Get camera image
266
+ image = self.get_camera_image()
267
+
268
+ # Run detection model
269
+ detections = self.detect_objects(image)
270
+
271
+ # Filter by confidence threshold
272
+ confident_detections = [
273
+ d for d in detections
274
+ if d['confidence'] > self.params['detection_threshold']
275
+ ]
276
+
277
+ # Log results
278
+ self.log_detections(confident_detections)
279
+
280
+ return len(confident_detections) > 0
281
+ '''
282
+ else:
283
+ implementation = '''
284
+ def execute(self):
285
+ """Execute skill."""
286
+ # TODO: Implement skill logic
287
+
288
+ # Your implementation here
289
+ pass
290
+
291
+ return True
292
+ '''
293
+
294
+ return f'''#!/usr/bin/env python3
295
+ """
296
+ {template.description}
297
+
298
+ Task: {task_description}
299
+
300
+ Generated by FoodforThought CLI
301
+ """
302
+
303
+ import numpy as np
304
+ from typing import Dict, List, Optional
305
+
306
+
307
+ class Skill:
308
+ """
309
+ {template.description}
310
+
311
+ This class implements the core skill logic.
312
+ """
313
+
314
+ def __init__(self, params: Dict):
315
+ """Initialize skill with parameters from skill.yaml."""
316
+ self.params = params
317
+ self.robot = None
318
+ self.logger = None
319
+
320
+ def setup(self, robot, logger=None):
321
+ """Setup skill with robot interface and logger."""
322
+ self.robot = robot
323
+ self.logger = logger
324
+
325
+ def validate(self) -> bool:
326
+ """Validate that skill can be executed."""
327
+ # TODO: Add validation checks
328
+ if self.robot is None:
329
+ return False
330
+ return True
331
+ {implementation}
332
+
333
+ # Helper methods - TODO: Implement based on robot interface
334
+
335
+ def move_to(self, pose):
336
+ """Move robot to target pose."""
337
+ # TODO: Implement motion control
338
+ pass
339
+
340
+ def close_gripper(self):
341
+ """Close gripper."""
342
+ # TODO: Implement gripper control
343
+ pass
344
+
345
+ def open_gripper(self):
346
+ """Open gripper."""
347
+ # TODO: Implement gripper control
348
+ pass
349
+
350
+ def get_camera_image(self):
351
+ """Get current camera image."""
352
+ # TODO: Implement camera interface
353
+ return None
354
+
355
+
356
+ def main():
357
+ """Main entry point for testing."""
358
+ import yaml
359
+
360
+ # Load configuration
361
+ with open("skill.yaml") as f:
362
+ config = yaml.safe_load(f)
363
+
364
+ # Create and run skill
365
+ skill = Skill(config.get("parameters", {{}}))
366
+
367
+ # TODO: Setup robot interface for testing
368
+ # skill.setup(robot)
369
+
370
+ if skill.validate():
371
+ success = skill.execute()
372
+ print(f"Skill execution: {{'success' if success else 'failed'}}")
373
+ else:
374
+ print("Skill validation failed")
375
+
376
+
377
+ if __name__ == "__main__":
378
+ main()
379
+ '''
380
+
381
+
382
+ def generate_test_py(template: SkillTemplate, task_description: str) -> str:
383
+ """Generate test_skill.py test file."""
384
+
385
+ return f'''#!/usr/bin/env python3
386
+ """
387
+ Tests for: {task_description}
388
+
389
+ Run with: pytest test_skill.py -v
390
+ """
391
+
392
+ import pytest
393
+ import numpy as np
394
+ from main import Skill
395
+
396
+
397
+ class MockRobot:
398
+ """Mock robot interface for testing."""
399
+
400
+ def __init__(self):
401
+ self.position = np.array([0.0, 0.0, 0.0])
402
+ self.gripper_closed = False
403
+ self.move_history = []
404
+
405
+ def get_position(self):
406
+ return self.position.copy()
407
+
408
+ def move_to(self, pose):
409
+ self.move_history.append(pose)
410
+ self.position = np.array(pose[:3])
411
+ return True
412
+
413
+ def close_gripper(self):
414
+ self.gripper_closed = True
415
+ return True
416
+
417
+ def open_gripper(self):
418
+ self.gripper_closed = False
419
+ return True
420
+
421
+
422
+ @pytest.fixture
423
+ def skill():
424
+ """Create skill instance with default parameters."""
425
+ params = {{
426
+ {_format_params_for_test(template.parameters)}
427
+ }}
428
+ return Skill(params)
429
+
430
+
431
+ @pytest.fixture
432
+ def mock_robot():
433
+ """Create mock robot instance."""
434
+ return MockRobot()
435
+
436
+
437
+ class TestSkillValidation:
438
+ """Test skill validation."""
439
+
440
+ def test_validate_without_robot(self, skill):
441
+ """Skill should not validate without robot setup."""
442
+ assert skill.validate() == False
443
+
444
+ def test_validate_with_robot(self, skill, mock_robot):
445
+ """Skill should validate with robot setup."""
446
+ skill.setup(mock_robot)
447
+ assert skill.validate() == True
448
+
449
+
450
+ class TestSkillExecution:
451
+ """Test skill execution."""
452
+
453
+ def test_execute_basic(self, skill, mock_robot):
454
+ """Basic execution should succeed."""
455
+ skill.setup(mock_robot)
456
+ # TODO: Add proper test implementation
457
+ # result = skill.execute()
458
+ # assert result == True
459
+ pass
460
+
461
+ def test_execute_logs_actions(self, skill, mock_robot):
462
+ """Execution should log all actions."""
463
+ skill.setup(mock_robot)
464
+ # TODO: Add action logging test
465
+ pass
466
+
467
+
468
+ class TestSkillParameters:
469
+ """Test skill parameter handling."""
470
+
471
+ def test_default_parameters(self, skill):
472
+ """Skill should have default parameters."""
473
+ assert skill.params is not None
474
+
475
+ def test_parameter_override(self):
476
+ """Parameters should be overridable."""
477
+ custom_params = {{"test_param": 123}}
478
+ skill = Skill(custom_params)
479
+ assert skill.params.get("test_param") == 123
480
+
481
+
482
+ class TestSafetyConstraints:
483
+ """Test safety constraints."""
484
+
485
+ def test_velocity_limit(self, skill, mock_robot):
486
+ """Skill should respect velocity limits."""
487
+ skill.setup(mock_robot)
488
+ # TODO: Add velocity limit test
489
+ pass
490
+
491
+ def test_workspace_bounds(self, skill, mock_robot):
492
+ """Skill should stay within workspace bounds."""
493
+ skill.setup(mock_robot)
494
+ # TODO: Add workspace bounds test
495
+ pass
496
+
497
+
498
+ if __name__ == "__main__":
499
+ pytest.main([__file__, "-v"])
500
+ '''
501
+
502
+
503
+ def _format_params_for_test(params: Dict) -> str:
504
+ """Format parameters for test fixture."""
505
+ lines = []
506
+ for key, value in params.items():
507
+ if isinstance(value, str):
508
+ lines.append(f' "{key}": "{value}",')
509
+ elif isinstance(value, bool):
510
+ lines.append(f' "{key}": {str(value)},')
511
+ else:
512
+ lines.append(f' "{key}": {value},')
513
+ return '\n'.join(lines)
514
+
515
+
516
+ def generate_readme(template: SkillTemplate, task_description: str,
517
+ robot_model: str) -> str:
518
+ """Generate README.md documentation file."""
519
+
520
+ return f'''# {task_description.title()}
521
+
522
+ {template.description}
523
+
524
+ ## Overview
525
+
526
+ This skill was generated using the FoodforThought CLI from the task description:
527
+
528
+ > "{task_description}"
529
+
530
+ ## Requirements
531
+
532
+ - Robot: `{robot_model}`
533
+ - Category: `{template.category}`
534
+ - FoodforThought CLI: `pip install foodforthought-cli`
535
+
536
+ ## Installation
537
+
538
+ ```bash
539
+ # Clone this skill
540
+ ate clone <skill-id>
541
+
542
+ # Or use in your training script
543
+ ate pull <skill-id> --format rlds --output ./data/
544
+ ```
545
+
546
+ ## Usage
547
+
548
+ ### Python
549
+
550
+ ```python
551
+ from main import Skill
552
+ import yaml
553
+
554
+ # Load configuration
555
+ with open("skill.yaml") as f:
556
+ config = yaml.safe_load(f)
557
+
558
+ # Initialize and run skill
559
+ skill = Skill(config["parameters"])
560
+ skill.setup(your_robot_interface)
561
+
562
+ if skill.validate():
563
+ success = skill.execute()
564
+ ```
565
+
566
+ ### CLI
567
+
568
+ ```bash
569
+ # Validate skill
570
+ ate validate --checks collision speed workspace
571
+
572
+ # Test in simulation
573
+ ate test -e pybullet -r {robot_model}
574
+
575
+ # Check transfer compatibility
576
+ ate check-transfer --from {robot_model} --to <target-robot>
577
+ ```
578
+
579
+ ## Configuration
580
+
581
+ See `skill.yaml` for configurable parameters:
582
+
583
+ | Parameter | Description | Default |
584
+ |-----------|-------------|---------|
585
+ ''' + '\n'.join([f'| `{param}` | TODO: Add description | `{value}` |' for param, value in template.parameters.items()]) + '''
586
+
587
+ ## Testing
588
+
589
+ ```bash
590
+ # Run unit tests
591
+ pytest test_skill.py -v
592
+
593
+ # Run simulation tests
594
+ ate test -e pybullet --trials 10
595
+ ```
596
+
597
+ ## Contributing
598
+
599
+ 1. Fork this skill
600
+ 2. Make improvements
601
+ 3. Submit demonstration videos for community labeling
602
+ 4. Create a pull request
603
+
604
+ ## License
605
+
606
+ MIT License - See LICENSE file for details.
607
+
608
+ ---
609
+
610
+ Generated by [FoodforThought CLI](https://kindly.fyi/foodforthought)
611
+ '''
612
+
613
+
614
+ def generate_skill_project(task_description: str, robot_model: str,
615
+ output_dir: str) -> Dict[str, str]:
616
+ """
617
+ Generate a complete skill project from a task description.
618
+
619
+ Args:
620
+ task_description: Natural language description of the task
621
+ robot_model: Target robot model
622
+ output_dir: Directory to create skill in
623
+
624
+ Returns:
625
+ Dict mapping file paths to their contents
626
+ """
627
+ # Parse task description
628
+ template_name, extracted_params = parse_task_description(task_description)
629
+ template = TEMPLATES[template_name]
630
+
631
+ # Generate files
632
+ files = {
633
+ "skill.yaml": generate_skill_yaml(template, task_description,
634
+ robot_model, extracted_params),
635
+ "main.py": generate_main_py(template, task_description),
636
+ "test_skill.py": generate_test_py(template, task_description),
637
+ "README.md": generate_readme(template, task_description, robot_model),
638
+ }
639
+
640
+ # Create output directory
641
+ output_path = Path(output_dir)
642
+ output_path.mkdir(parents=True, exist_ok=True)
643
+
644
+ # Write files
645
+ for filename, content in files.items():
646
+ file_path = output_path / filename
647
+ with open(file_path, 'w') as f:
648
+ f.write(content)
649
+
650
+ # Create additional directories
651
+ (output_path / "data").mkdir(exist_ok=True)
652
+ (output_path / "models").mkdir(exist_ok=True)
653
+
654
+ # Create .gitignore
655
+ with open(output_path / ".gitignore", 'w') as f:
656
+ f.write("""# Python
657
+ __pycache__/
658
+ *.py[cod]
659
+ *.egg-info/
660
+ .eggs/
661
+ dist/
662
+ build/
663
+
664
+ # Data
665
+ data/
666
+ *.h5
667
+ *.hdf5
668
+ *.npy
669
+ *.npz
670
+
671
+ # Models
672
+ models/
673
+ *.pth
674
+ *.pt
675
+ *.onnx
676
+
677
+ # IDE
678
+ .vscode/
679
+ .idea/
680
+
681
+ # OS
682
+ .DS_Store
683
+ Thumbs.db
684
+
685
+ # Logs
686
+ *.log
687
+ logs/
688
+
689
+ # Virtual environment
690
+ venv/
691
+ .venv/
692
+ """)
693
+
694
+ return {
695
+ "template": template_name,
696
+ "files_created": list(files.keys()) + [".gitignore"],
697
+ "output_dir": str(output_path),
698
+ "extracted_params": extracted_params,
699
+ }
700
+
701
+
702
+ if __name__ == "__main__":
703
+ # Test the generator
704
+ result = generate_skill_project(
705
+ task_description="pick up the red box and place it on the table",
706
+ robot_model="franka-panda",
707
+ output_dir="./test-skill"
708
+ )
709
+ print(f"Generated skill project:")
710
+ print(f" Template: {result['template']}")
711
+ print(f" Files: {result['files_created']}")
712
+ print(f" Output: {result['output_dir']}")
713
+