foodforthought-cli 0.2.1__py3-none-any.whl → 0.2.4__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 +1 -1
- ate/bridge_server.py +622 -0
- ate/cli.py +2625 -242
- ate/compatibility.py +580 -0
- ate/generators/__init__.py +19 -0
- ate/generators/docker_generator.py +461 -0
- ate/generators/hardware_config.py +469 -0
- ate/generators/ros2_generator.py +617 -0
- ate/generators/skill_generator.py +783 -0
- ate/marketplace.py +524 -0
- ate/mcp_server.py +1341 -107
- ate/primitives.py +1016 -0
- ate/robot_setup.py +2222 -0
- ate/skill_schema.py +537 -0
- ate/telemetry/__init__.py +33 -0
- ate/telemetry/cli.py +455 -0
- ate/telemetry/collector.py +444 -0
- ate/telemetry/context.py +318 -0
- ate/telemetry/fleet_agent.py +419 -0
- ate/telemetry/formats/__init__.py +18 -0
- ate/telemetry/formats/hdf5_serializer.py +503 -0
- ate/telemetry/formats/mcap_serializer.py +457 -0
- ate/telemetry/types.py +334 -0
- foodforthought_cli-0.2.4.dist-info/METADATA +300 -0
- foodforthought_cli-0.2.4.dist-info/RECORD +44 -0
- foodforthought_cli-0.2.4.dist-info/top_level.txt +6 -0
- mechdog_labeled/__init__.py +3 -0
- mechdog_labeled/primitives.py +113 -0
- mechdog_labeled/servo_map.py +209 -0
- mechdog_output/__init__.py +3 -0
- mechdog_output/primitives.py +59 -0
- mechdog_output/servo_map.py +203 -0
- test_autodetect/__init__.py +3 -0
- test_autodetect/primitives.py +113 -0
- test_autodetect/servo_map.py +209 -0
- test_full_auto/__init__.py +3 -0
- test_full_auto/primitives.py +113 -0
- test_full_auto/servo_map.py +209 -0
- test_smart_detect/__init__.py +3 -0
- test_smart_detect/primitives.py +113 -0
- test_smart_detect/servo_map.py +209 -0
- foodforthought_cli-0.2.1.dist-info/METADATA +0 -151
- foodforthought_cli-0.2.1.dist-info/RECORD +0 -9
- foodforthought_cli-0.2.1.dist-info/top_level.txt +0 -1
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.4.dist-info}/WHEEL +0 -0
- {foodforthought_cli-0.2.1.dist-info → foodforthought_cli-0.2.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill Code Generator - Generate Python skill implementations from specifications.
|
|
3
|
+
|
|
4
|
+
This generator creates:
|
|
5
|
+
- skill.py: Main skill class with execute() method
|
|
6
|
+
- primitives.py: Hardware-abstracted primitive wrappers
|
|
7
|
+
- validators.py: Input/output validation
|
|
8
|
+
- state_machine.py: Execution flow state machine
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from ..skill_schema import SkillSpecification, SkillParameter, PrimitiveCall
|
|
17
|
+
from ..primitives import PRIMITIVE_REGISTRY, get_primitive
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def to_pascal_case(name: str) -> str:
|
|
21
|
+
"""Convert snake_case or kebab-case to PascalCase."""
|
|
22
|
+
# Replace hyphens with underscores
|
|
23
|
+
name = name.replace("-", "_")
|
|
24
|
+
# Split on underscores and capitalize each word
|
|
25
|
+
return "".join(word.capitalize() for word in name.split("_"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def to_snake_case(name: str) -> str:
|
|
29
|
+
"""Convert PascalCase or kebab-case to snake_case."""
|
|
30
|
+
# Replace hyphens with underscores
|
|
31
|
+
name = name.replace("-", "_")
|
|
32
|
+
# Insert underscore before uppercase letters
|
|
33
|
+
name = re.sub(r"(?<!^)(?=[A-Z])", "_", name)
|
|
34
|
+
return name.lower()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def python_type(param_type: str) -> str:
|
|
38
|
+
"""Convert skill parameter type to Python type annotation."""
|
|
39
|
+
type_map = {
|
|
40
|
+
"Pose": "Dict[str, Any]",
|
|
41
|
+
"float": "float",
|
|
42
|
+
"int": "int",
|
|
43
|
+
"bool": "bool",
|
|
44
|
+
"string": "str",
|
|
45
|
+
"array": "List[Any]",
|
|
46
|
+
"JointState": "List[float]",
|
|
47
|
+
"Trajectory": "List[Dict[str, Any]]",
|
|
48
|
+
"Point": "Tuple[float, float, float]",
|
|
49
|
+
"Quaternion": "Tuple[float, float, float, float]",
|
|
50
|
+
"Transform": "Dict[str, Any]",
|
|
51
|
+
}
|
|
52
|
+
return type_map.get(param_type, "Any")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SkillCodeGenerator:
|
|
56
|
+
"""
|
|
57
|
+
Generate Python code for a skill from its specification.
|
|
58
|
+
|
|
59
|
+
This generator creates a complete Python package structure with:
|
|
60
|
+
- Main skill class
|
|
61
|
+
- Primitive wrappers
|
|
62
|
+
- Input/output validators
|
|
63
|
+
- State machine for execution flow
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, spec: SkillSpecification):
|
|
67
|
+
"""
|
|
68
|
+
Initialize the generator.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
spec: The skill specification to generate code for
|
|
72
|
+
"""
|
|
73
|
+
self.spec = spec
|
|
74
|
+
self.class_name = to_pascal_case(spec.name)
|
|
75
|
+
self.module_name = to_snake_case(spec.name)
|
|
76
|
+
|
|
77
|
+
def generate_skill_class(self) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Generate the main skill.py file.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Python source code for the skill class
|
|
83
|
+
"""
|
|
84
|
+
lines = [
|
|
85
|
+
'"""',
|
|
86
|
+
f'{self.spec.name} - {self.spec.description}',
|
|
87
|
+
'',
|
|
88
|
+
f'Generated by Skill Compiler v1.0.0',
|
|
89
|
+
f'Generated at: {datetime.now().isoformat()}',
|
|
90
|
+
'',
|
|
91
|
+
'Do not edit manually - regenerate from skill.yaml',
|
|
92
|
+
'"""',
|
|
93
|
+
'',
|
|
94
|
+
'from dataclasses import dataclass',
|
|
95
|
+
'from typing import Any, Dict, List, Optional, Tuple',
|
|
96
|
+
'import logging',
|
|
97
|
+
'',
|
|
98
|
+
f'from .primitives import PrimitiveExecutor',
|
|
99
|
+
f'from .validators import validate_{self.module_name}_input, validate_{self.module_name}_output',
|
|
100
|
+
'',
|
|
101
|
+
'',
|
|
102
|
+
'logger = logging.getLogger(__name__)',
|
|
103
|
+
'',
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
# Generate input dataclass
|
|
107
|
+
lines.extend(self._generate_input_class())
|
|
108
|
+
lines.append('')
|
|
109
|
+
|
|
110
|
+
# Generate output dataclass
|
|
111
|
+
lines.extend(self._generate_output_class())
|
|
112
|
+
lines.append('')
|
|
113
|
+
|
|
114
|
+
# Generate main skill class
|
|
115
|
+
lines.extend(self._generate_skill_class_body())
|
|
116
|
+
|
|
117
|
+
return '\n'.join(lines)
|
|
118
|
+
|
|
119
|
+
def _generate_input_class(self) -> List[str]:
|
|
120
|
+
"""Generate the input dataclass."""
|
|
121
|
+
lines = [
|
|
122
|
+
'@dataclass',
|
|
123
|
+
f'class {self.class_name}Input:',
|
|
124
|
+
f' """Input parameters for {self.spec.name} skill."""',
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
if not self.spec.parameters:
|
|
128
|
+
lines.append(' pass')
|
|
129
|
+
return lines
|
|
130
|
+
|
|
131
|
+
for param in self.spec.parameters:
|
|
132
|
+
type_annotation = python_type(param.type)
|
|
133
|
+
if param.default is not None:
|
|
134
|
+
default_str = repr(param.default)
|
|
135
|
+
lines.append(f' {param.name}: {type_annotation} = {default_str}')
|
|
136
|
+
elif not param.required:
|
|
137
|
+
lines.append(f' {param.name}: Optional[{type_annotation}] = None')
|
|
138
|
+
else:
|
|
139
|
+
lines.append(f' {param.name}: {type_annotation}')
|
|
140
|
+
|
|
141
|
+
return lines
|
|
142
|
+
|
|
143
|
+
def _generate_output_class(self) -> List[str]:
|
|
144
|
+
"""Generate the output dataclass."""
|
|
145
|
+
lines = [
|
|
146
|
+
'@dataclass',
|
|
147
|
+
f'class {self.class_name}Output:',
|
|
148
|
+
f' """Output from {self.spec.name} skill execution."""',
|
|
149
|
+
' success: bool',
|
|
150
|
+
' message: str',
|
|
151
|
+
' error_code: Optional[str] = None',
|
|
152
|
+
' execution_time: float = 0.0',
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
# Add success criteria as output fields
|
|
156
|
+
for criterion in self.spec.success_criteria:
|
|
157
|
+
lines.append(f' {criterion.name}: bool = False')
|
|
158
|
+
|
|
159
|
+
return lines
|
|
160
|
+
|
|
161
|
+
def _generate_skill_class_body(self) -> List[str]:
|
|
162
|
+
"""Generate the main skill class."""
|
|
163
|
+
lines = [
|
|
164
|
+
f'class {self.class_name}Skill:',
|
|
165
|
+
f' """',
|
|
166
|
+
f' {self.spec.description}',
|
|
167
|
+
f' ',
|
|
168
|
+
f' Version: {self.spec.version}',
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
if self.spec.author:
|
|
172
|
+
lines.append(f' Author: {self.spec.author}')
|
|
173
|
+
|
|
174
|
+
lines.extend([
|
|
175
|
+
f' """',
|
|
176
|
+
'',
|
|
177
|
+
' def __init__(self, hardware_config: Dict[str, Any]):',
|
|
178
|
+
' """',
|
|
179
|
+
' Initialize the skill with hardware configuration.',
|
|
180
|
+
' ',
|
|
181
|
+
' Args:',
|
|
182
|
+
' hardware_config: Hardware mapping configuration',
|
|
183
|
+
' """',
|
|
184
|
+
' self.config = hardware_config',
|
|
185
|
+
' self.primitives = PrimitiveExecutor(hardware_config)',
|
|
186
|
+
' self._setup_hardware()',
|
|
187
|
+
'',
|
|
188
|
+
' def _setup_hardware(self) -> None:',
|
|
189
|
+
' """Initialize hardware components."""',
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
# Add hardware setup for each requirement
|
|
193
|
+
for req in self.spec.hardware_requirements:
|
|
194
|
+
comp_type = req.component_type
|
|
195
|
+
lines.extend([
|
|
196
|
+
f' self.{comp_type} = self.config.get("{comp_type}")',
|
|
197
|
+
f' if not self.{comp_type}:',
|
|
198
|
+
f' logger.warning("No {comp_type} configured")',
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
lines.extend([
|
|
202
|
+
'',
|
|
203
|
+
f' def execute(self, input: {self.class_name}Input) -> {self.class_name}Output:',
|
|
204
|
+
' """',
|
|
205
|
+
' Execute the skill.',
|
|
206
|
+
' ',
|
|
207
|
+
' Args:',
|
|
208
|
+
' input: Skill input parameters',
|
|
209
|
+
' ',
|
|
210
|
+
' Returns:',
|
|
211
|
+
' Skill execution output with success status',
|
|
212
|
+
' """',
|
|
213
|
+
' import time',
|
|
214
|
+
' start_time = time.time()',
|
|
215
|
+
'',
|
|
216
|
+
' # Validate input',
|
|
217
|
+
f' validation_errors = validate_{self.module_name}_input(input)',
|
|
218
|
+
' if validation_errors:',
|
|
219
|
+
f' return {self.class_name}Output(',
|
|
220
|
+
' success=False,',
|
|
221
|
+
' message=f"Input validation failed: {validation_errors}",',
|
|
222
|
+
' error_code="INPUT_VALIDATION_ERROR"',
|
|
223
|
+
' )',
|
|
224
|
+
'',
|
|
225
|
+
' try:',
|
|
226
|
+
' # Execute skill logic',
|
|
227
|
+
' result = self._execute_impl(input)',
|
|
228
|
+
' execution_time = time.time() - start_time',
|
|
229
|
+
'',
|
|
230
|
+
' # Build output',
|
|
231
|
+
f' output = {self.class_name}Output(',
|
|
232
|
+
' success=result.get("success", False),',
|
|
233
|
+
' message=result.get("message", ""),',
|
|
234
|
+
' execution_time=execution_time,',
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
# Add success criteria outputs
|
|
238
|
+
for criterion in self.spec.success_criteria:
|
|
239
|
+
lines.append(f' {criterion.name}=result.get("{criterion.name}", False),')
|
|
240
|
+
|
|
241
|
+
lines.extend([
|
|
242
|
+
' )',
|
|
243
|
+
'',
|
|
244
|
+
' # Validate output',
|
|
245
|
+
f' output_errors = validate_{self.module_name}_output(output)',
|
|
246
|
+
' if output_errors:',
|
|
247
|
+
' logger.warning(f"Output validation warnings: {output_errors}")',
|
|
248
|
+
'',
|
|
249
|
+
' return output',
|
|
250
|
+
'',
|
|
251
|
+
' except Exception as e:',
|
|
252
|
+
' execution_time = time.time() - start_time',
|
|
253
|
+
' logger.error(f"Skill execution failed: {e}")',
|
|
254
|
+
f' return {self.class_name}Output(',
|
|
255
|
+
' success=False,',
|
|
256
|
+
' message=str(e),',
|
|
257
|
+
' error_code="EXECUTION_ERROR",',
|
|
258
|
+
' execution_time=execution_time',
|
|
259
|
+
' )',
|
|
260
|
+
'',
|
|
261
|
+
])
|
|
262
|
+
|
|
263
|
+
# Generate implementation method
|
|
264
|
+
lines.extend(self._generate_execute_impl())
|
|
265
|
+
|
|
266
|
+
return lines
|
|
267
|
+
|
|
268
|
+
def _generate_execute_impl(self) -> List[str]:
|
|
269
|
+
"""Generate the _execute_impl method with skill logic."""
|
|
270
|
+
lines = [
|
|
271
|
+
f' def _execute_impl(self, input: {self.class_name}Input) -> Dict[str, Any]:',
|
|
272
|
+
' """',
|
|
273
|
+
' Internal skill implementation.',
|
|
274
|
+
' ',
|
|
275
|
+
' Override this method to customize skill behavior.',
|
|
276
|
+
' """',
|
|
277
|
+
' result = {"success": True, "message": ""}',
|
|
278
|
+
'',
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
# Generate execution flow from primitives
|
|
282
|
+
if self.spec.execution_flow:
|
|
283
|
+
lines.append(' # Execute primitive sequence')
|
|
284
|
+
for i, call in enumerate(self.spec.execution_flow):
|
|
285
|
+
lines.extend(self._generate_primitive_call(call, i))
|
|
286
|
+
elif self.spec.primitives:
|
|
287
|
+
# Default execution: call each primitive in order
|
|
288
|
+
lines.append(' # Default execution sequence')
|
|
289
|
+
for i, prim_name in enumerate(self.spec.primitives):
|
|
290
|
+
call = PrimitiveCall(primitive=prim_name)
|
|
291
|
+
lines.extend(self._generate_primitive_call(call, i))
|
|
292
|
+
else:
|
|
293
|
+
lines.extend([
|
|
294
|
+
' # TODO: Implement skill logic',
|
|
295
|
+
' logger.warning("Skill implementation not complete")',
|
|
296
|
+
])
|
|
297
|
+
|
|
298
|
+
# Check success criteria
|
|
299
|
+
lines.append('')
|
|
300
|
+
lines.append(' # Evaluate success criteria')
|
|
301
|
+
for criterion in self.spec.success_criteria:
|
|
302
|
+
lines.extend([
|
|
303
|
+
f' result["{criterion.name}"] = self._check_{criterion.name}(input)',
|
|
304
|
+
])
|
|
305
|
+
|
|
306
|
+
lines.extend([
|
|
307
|
+
'',
|
|
308
|
+
' # Overall success is all criteria met',
|
|
309
|
+
' all_criteria = [',
|
|
310
|
+
])
|
|
311
|
+
for criterion in self.spec.success_criteria:
|
|
312
|
+
lines.append(f' result.get("{criterion.name}", False),')
|
|
313
|
+
lines.extend([
|
|
314
|
+
' ]',
|
|
315
|
+
' result["success"] = all(all_criteria) if all_criteria else True',
|
|
316
|
+
' result["message"] = "Skill completed successfully" if result["success"] else "Some criteria not met"',
|
|
317
|
+
'',
|
|
318
|
+
' return result',
|
|
319
|
+
'',
|
|
320
|
+
])
|
|
321
|
+
|
|
322
|
+
# Generate success criterion check methods
|
|
323
|
+
for criterion in self.spec.success_criteria:
|
|
324
|
+
lines.extend([
|
|
325
|
+
f' def _check_{criterion.name}(self, input: {self.class_name}Input) -> bool:',
|
|
326
|
+
f' """Check if {criterion.name} criterion is met."""',
|
|
327
|
+
f' # Condition: {criterion.condition}',
|
|
328
|
+
])
|
|
329
|
+
if criterion.tolerance is not None:
|
|
330
|
+
lines.append(f' # Tolerance: {criterion.tolerance}')
|
|
331
|
+
lines.extend([
|
|
332
|
+
' # TODO: Implement criterion check',
|
|
333
|
+
' return True',
|
|
334
|
+
'',
|
|
335
|
+
])
|
|
336
|
+
|
|
337
|
+
return lines
|
|
338
|
+
|
|
339
|
+
def _generate_primitive_call(self, call: PrimitiveCall, index: int) -> List[str]:
|
|
340
|
+
"""Generate code for a single primitive call."""
|
|
341
|
+
lines = []
|
|
342
|
+
|
|
343
|
+
# Add condition check if present
|
|
344
|
+
if call.condition:
|
|
345
|
+
lines.append(f' if {call.condition}:')
|
|
346
|
+
indent = ' '
|
|
347
|
+
else:
|
|
348
|
+
indent = ' '
|
|
349
|
+
|
|
350
|
+
# Generate the primitive call
|
|
351
|
+
prim = get_primitive(call.primitive)
|
|
352
|
+
if prim:
|
|
353
|
+
# Build parameter string
|
|
354
|
+
params = []
|
|
355
|
+
for param_name, param_value in call.parameters.items():
|
|
356
|
+
if isinstance(param_value, str) and param_value.startswith('input.'):
|
|
357
|
+
params.append(f'{param_name}={param_value}')
|
|
358
|
+
else:
|
|
359
|
+
params.append(f'{param_name}={repr(param_value)}')
|
|
360
|
+
|
|
361
|
+
param_str = ', '.join(params)
|
|
362
|
+
lines.append(f'{indent}step_{index}_result = self.primitives.{call.primitive}({param_str})')
|
|
363
|
+
else:
|
|
364
|
+
lines.append(f'{indent}# Unknown primitive: {call.primitive}')
|
|
365
|
+
lines.append(f'{indent}step_{index}_result = True')
|
|
366
|
+
|
|
367
|
+
# Handle failure
|
|
368
|
+
if call.on_failure:
|
|
369
|
+
lines.extend([
|
|
370
|
+
f'{indent}if not step_{index}_result:',
|
|
371
|
+
])
|
|
372
|
+
if call.on_failure == 'abort':
|
|
373
|
+
lines.append(f'{indent} raise RuntimeError("Primitive {call.primitive} failed")')
|
|
374
|
+
elif call.on_failure == 'retry' and call.retries > 0:
|
|
375
|
+
lines.extend([
|
|
376
|
+
f'{indent} for retry in range({call.retries}):',
|
|
377
|
+
f'{indent} step_{index}_result = self.primitives.{call.primitive}()',
|
|
378
|
+
f'{indent} if step_{index}_result:',
|
|
379
|
+
f'{indent} break',
|
|
380
|
+
])
|
|
381
|
+
# 'continue' just continues execution
|
|
382
|
+
|
|
383
|
+
lines.append('')
|
|
384
|
+
return lines
|
|
385
|
+
|
|
386
|
+
def generate_primitives_wrapper(self) -> str:
|
|
387
|
+
"""
|
|
388
|
+
Generate primitives.py with hardware-abstracted primitive calls.
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
Python source code for the primitives wrapper
|
|
392
|
+
"""
|
|
393
|
+
lines = [
|
|
394
|
+
'"""',
|
|
395
|
+
f'Primitives wrapper for {self.spec.name} skill.',
|
|
396
|
+
'',
|
|
397
|
+
'Provides hardware-abstracted calls to robot primitives.',
|
|
398
|
+
f'Generated by Skill Compiler v1.0.0',
|
|
399
|
+
'"""',
|
|
400
|
+
'',
|
|
401
|
+
'from typing import Any, Dict, List, Optional',
|
|
402
|
+
'import logging',
|
|
403
|
+
'',
|
|
404
|
+
'',
|
|
405
|
+
'logger = logging.getLogger(__name__)',
|
|
406
|
+
'',
|
|
407
|
+
'',
|
|
408
|
+
'class PrimitiveExecutor:',
|
|
409
|
+
' """',
|
|
410
|
+
' Execute primitives with hardware abstraction.',
|
|
411
|
+
' ',
|
|
412
|
+
' Wraps low-level hardware calls and provides a consistent interface.',
|
|
413
|
+
' """',
|
|
414
|
+
'',
|
|
415
|
+
' def __init__(self, hardware_config: Dict[str, Any]):',
|
|
416
|
+
' """Initialize with hardware configuration."""',
|
|
417
|
+
' self.config = hardware_config',
|
|
418
|
+
' self._setup_drivers()',
|
|
419
|
+
'',
|
|
420
|
+
' def _setup_drivers(self) -> None:',
|
|
421
|
+
' """Setup hardware drivers based on config."""',
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
# Setup hardware drivers
|
|
425
|
+
for req in self.spec.hardware_requirements:
|
|
426
|
+
comp_type = req.component_type
|
|
427
|
+
lines.extend([
|
|
428
|
+
f' self.{comp_type}_driver = self._get_{comp_type}_driver()',
|
|
429
|
+
])
|
|
430
|
+
|
|
431
|
+
lines.append('')
|
|
432
|
+
|
|
433
|
+
# Generate driver getter methods
|
|
434
|
+
for req in self.spec.hardware_requirements:
|
|
435
|
+
comp_type = req.component_type
|
|
436
|
+
lines.extend([
|
|
437
|
+
f' def _get_{comp_type}_driver(self):',
|
|
438
|
+
f' """Get or create {comp_type} hardware driver."""',
|
|
439
|
+
f' config = self.config.get("{comp_type}", {{}})',
|
|
440
|
+
f' driver_type = config.get("driver", "mock")',
|
|
441
|
+
' # TODO: Implement driver factory',
|
|
442
|
+
' return MockDriver()',
|
|
443
|
+
'',
|
|
444
|
+
])
|
|
445
|
+
|
|
446
|
+
# Generate primitive methods
|
|
447
|
+
for prim_name in self.spec.primitives:
|
|
448
|
+
prim = get_primitive(prim_name)
|
|
449
|
+
if prim:
|
|
450
|
+
lines.extend(self._generate_primitive_method(prim))
|
|
451
|
+
else:
|
|
452
|
+
lines.extend([
|
|
453
|
+
f' def {prim_name}(self, **kwargs) -> bool:',
|
|
454
|
+
f' """Unknown primitive: {prim_name}"""',
|
|
455
|
+
f' logger.warning("Primitive {prim_name} not implemented")',
|
|
456
|
+
' return False',
|
|
457
|
+
'',
|
|
458
|
+
])
|
|
459
|
+
|
|
460
|
+
# Add mock driver class
|
|
461
|
+
lines.extend([
|
|
462
|
+
'',
|
|
463
|
+
'class MockDriver:',
|
|
464
|
+
' """Mock hardware driver for testing."""',
|
|
465
|
+
'',
|
|
466
|
+
' def __getattr__(self, name):',
|
|
467
|
+
' def mock_method(*args, **kwargs):',
|
|
468
|
+
' logger.info(f"Mock call: {name}({args}, {kwargs})")',
|
|
469
|
+
' return True',
|
|
470
|
+
' return mock_method',
|
|
471
|
+
])
|
|
472
|
+
|
|
473
|
+
return '\n'.join(lines)
|
|
474
|
+
|
|
475
|
+
def _generate_primitive_method(self, prim) -> List[str]:
|
|
476
|
+
"""Generate a method for a single primitive."""
|
|
477
|
+
lines = []
|
|
478
|
+
|
|
479
|
+
# Build parameter list
|
|
480
|
+
params = ['self']
|
|
481
|
+
for param_name, param_def in prim.parameters.items():
|
|
482
|
+
type_str = python_type(param_def.type)
|
|
483
|
+
if param_def.required:
|
|
484
|
+
params.append(f'{param_name}: {type_str}')
|
|
485
|
+
else:
|
|
486
|
+
default_val = repr(param_def.default)
|
|
487
|
+
params.append(f'{param_name}: {type_str} = {default_val}')
|
|
488
|
+
|
|
489
|
+
param_str = ', '.join(params)
|
|
490
|
+
return_type = python_type(prim.returns)
|
|
491
|
+
|
|
492
|
+
lines.extend([
|
|
493
|
+
f' def {prim.name}({param_str}) -> {return_type}:',
|
|
494
|
+
f' """',
|
|
495
|
+
f' {prim.description}',
|
|
496
|
+
f' ',
|
|
497
|
+
])
|
|
498
|
+
|
|
499
|
+
# Document parameters
|
|
500
|
+
for param_name, param_def in prim.parameters.items():
|
|
501
|
+
lines.append(f' Args:')
|
|
502
|
+
lines.append(f' {param_name}: {param_def.description}')
|
|
503
|
+
|
|
504
|
+
lines.extend([
|
|
505
|
+
f' ',
|
|
506
|
+
f' Returns:',
|
|
507
|
+
f' {prim.returns}: Success status',
|
|
508
|
+
f' """',
|
|
509
|
+
' try:',
|
|
510
|
+
])
|
|
511
|
+
|
|
512
|
+
# Generate hardware call based on primitive hardware requirements
|
|
513
|
+
if 'arm' in prim.hardware:
|
|
514
|
+
lines.append(' driver = self.arm_driver')
|
|
515
|
+
elif 'gripper' in prim.hardware:
|
|
516
|
+
lines.append(' driver = self.gripper_driver')
|
|
517
|
+
elif prim.hardware:
|
|
518
|
+
lines.append(f' driver = self.{prim.hardware[0]}_driver')
|
|
519
|
+
else:
|
|
520
|
+
lines.append(' driver = None')
|
|
521
|
+
|
|
522
|
+
# Generate the actual call
|
|
523
|
+
param_names = list(prim.parameters.keys())
|
|
524
|
+
call_args = ', '.join(f'{p}={p}' for p in param_names)
|
|
525
|
+
|
|
526
|
+
lines.extend([
|
|
527
|
+
f' result = driver.{prim.name}({call_args})',
|
|
528
|
+
' return result',
|
|
529
|
+
' except Exception as e:',
|
|
530
|
+
f' logger.error(f"Primitive {prim.name} failed: {{e}}")',
|
|
531
|
+
' return False' if prim.returns == 'bool' else ' return None',
|
|
532
|
+
'',
|
|
533
|
+
])
|
|
534
|
+
|
|
535
|
+
return lines
|
|
536
|
+
|
|
537
|
+
def generate_validators(self) -> str:
|
|
538
|
+
"""
|
|
539
|
+
Generate validators.py for input/output validation.
|
|
540
|
+
|
|
541
|
+
Returns:
|
|
542
|
+
Python source code for validators
|
|
543
|
+
"""
|
|
544
|
+
lines = [
|
|
545
|
+
'"""',
|
|
546
|
+
f'Validators for {self.spec.name} skill.',
|
|
547
|
+
'',
|
|
548
|
+
'Provides input and output validation.',
|
|
549
|
+
f'Generated by Skill Compiler v1.0.0',
|
|
550
|
+
'"""',
|
|
551
|
+
'',
|
|
552
|
+
'from typing import Any, Dict, List, Optional',
|
|
553
|
+
'',
|
|
554
|
+
'',
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
# Import the input/output classes
|
|
558
|
+
lines.extend([
|
|
559
|
+
f'def validate_{self.module_name}_input(input) -> List[str]:',
|
|
560
|
+
' """',
|
|
561
|
+
' Validate skill input parameters.',
|
|
562
|
+
' ',
|
|
563
|
+
' Args:',
|
|
564
|
+
' input: Input dataclass instance',
|
|
565
|
+
' ',
|
|
566
|
+
' Returns:',
|
|
567
|
+
' List of validation errors (empty if valid)',
|
|
568
|
+
' """',
|
|
569
|
+
' errors = []',
|
|
570
|
+
'',
|
|
571
|
+
])
|
|
572
|
+
|
|
573
|
+
# Generate validation for each parameter
|
|
574
|
+
for param in self.spec.parameters:
|
|
575
|
+
if param.required and param.default is None:
|
|
576
|
+
lines.extend([
|
|
577
|
+
f' # Validate {param.name}',
|
|
578
|
+
f' if input.{param.name} is None:',
|
|
579
|
+
f' errors.append("{param.name} is required")',
|
|
580
|
+
])
|
|
581
|
+
|
|
582
|
+
if param.range:
|
|
583
|
+
min_val, max_val = param.range
|
|
584
|
+
lines.extend([
|
|
585
|
+
f' if input.{param.name} is not None:',
|
|
586
|
+
f' if input.{param.name} < {min_val} or input.{param.name} > {max_val}:',
|
|
587
|
+
f' errors.append("{param.name} must be between {min_val} and {max_val}")',
|
|
588
|
+
])
|
|
589
|
+
|
|
590
|
+
lines.extend([
|
|
591
|
+
'',
|
|
592
|
+
' return errors',
|
|
593
|
+
'',
|
|
594
|
+
'',
|
|
595
|
+
])
|
|
596
|
+
|
|
597
|
+
# Output validation
|
|
598
|
+
lines.extend([
|
|
599
|
+
f'def validate_{self.module_name}_output(output) -> List[str]:',
|
|
600
|
+
' """',
|
|
601
|
+
' Validate skill output.',
|
|
602
|
+
' ',
|
|
603
|
+
' Args:',
|
|
604
|
+
' output: Output dataclass instance',
|
|
605
|
+
' ',
|
|
606
|
+
' Returns:',
|
|
607
|
+
' List of validation warnings',
|
|
608
|
+
' """',
|
|
609
|
+
' warnings = []',
|
|
610
|
+
'',
|
|
611
|
+
' if output.success and not output.message:',
|
|
612
|
+
' warnings.append("Success output should have a message")',
|
|
613
|
+
'',
|
|
614
|
+
' if not output.success and not output.error_code:',
|
|
615
|
+
' warnings.append("Failed output should have an error code")',
|
|
616
|
+
'',
|
|
617
|
+
' return warnings',
|
|
618
|
+
])
|
|
619
|
+
|
|
620
|
+
return '\n'.join(lines)
|
|
621
|
+
|
|
622
|
+
def generate_state_machine(self) -> str:
|
|
623
|
+
"""
|
|
624
|
+
Generate state_machine.py for execution flow.
|
|
625
|
+
|
|
626
|
+
Returns:
|
|
627
|
+
Python source code for state machine
|
|
628
|
+
"""
|
|
629
|
+
lines = [
|
|
630
|
+
'"""',
|
|
631
|
+
f'State machine for {self.spec.name} skill execution.',
|
|
632
|
+
'',
|
|
633
|
+
'Manages execution flow and state transitions.',
|
|
634
|
+
f'Generated by Skill Compiler v1.0.0',
|
|
635
|
+
'"""',
|
|
636
|
+
'',
|
|
637
|
+
'from enum import Enum, auto',
|
|
638
|
+
'from typing import Any, Callable, Dict, Optional',
|
|
639
|
+
'import logging',
|
|
640
|
+
'',
|
|
641
|
+
'',
|
|
642
|
+
'logger = logging.getLogger(__name__)',
|
|
643
|
+
'',
|
|
644
|
+
'',
|
|
645
|
+
f'class {self.class_name}State(Enum):',
|
|
646
|
+
' """States for skill execution."""',
|
|
647
|
+
' IDLE = auto()',
|
|
648
|
+
' INITIALIZING = auto()',
|
|
649
|
+
]
|
|
650
|
+
|
|
651
|
+
# Add states for each primitive
|
|
652
|
+
for i, prim in enumerate(self.spec.primitives):
|
|
653
|
+
state_name = prim.upper().replace('-', '_')
|
|
654
|
+
lines.append(f' {state_name} = auto()')
|
|
655
|
+
|
|
656
|
+
lines.extend([
|
|
657
|
+
' VALIDATING = auto()',
|
|
658
|
+
' COMPLETED = auto()',
|
|
659
|
+
' FAILED = auto()',
|
|
660
|
+
'',
|
|
661
|
+
'',
|
|
662
|
+
f'class {self.class_name}StateMachine:',
|
|
663
|
+
' """',
|
|
664
|
+
' State machine for managing skill execution flow.',
|
|
665
|
+
' """',
|
|
666
|
+
'',
|
|
667
|
+
' def __init__(self):',
|
|
668
|
+
f' self.state = {self.class_name}State.IDLE',
|
|
669
|
+
' self.error: Optional[str] = None',
|
|
670
|
+
' self._transitions: Dict[tuple, Callable] = {}',
|
|
671
|
+
' self._setup_transitions()',
|
|
672
|
+
'',
|
|
673
|
+
' def _setup_transitions(self) -> None:',
|
|
674
|
+
' """Define valid state transitions."""',
|
|
675
|
+
f' S = {self.class_name}State',
|
|
676
|
+
'',
|
|
677
|
+
' # Define transitions',
|
|
678
|
+
' self._transitions = {',
|
|
679
|
+
' (S.IDLE, "start"): S.INITIALIZING,',
|
|
680
|
+
])
|
|
681
|
+
|
|
682
|
+
# Add transitions between primitives
|
|
683
|
+
prev_state = 'INITIALIZING'
|
|
684
|
+
for prim in self.spec.primitives:
|
|
685
|
+
state_name = prim.upper().replace('-', '_')
|
|
686
|
+
lines.append(f' (S.{prev_state}, "next"): S.{state_name},')
|
|
687
|
+
prev_state = state_name
|
|
688
|
+
|
|
689
|
+
lines.extend([
|
|
690
|
+
f' (S.{prev_state}, "next"): S.VALIDATING,',
|
|
691
|
+
' (S.VALIDATING, "success"): S.COMPLETED,',
|
|
692
|
+
' (S.VALIDATING, "failure"): S.FAILED,',
|
|
693
|
+
' }',
|
|
694
|
+
'',
|
|
695
|
+
' def transition(self, event: str) -> bool:',
|
|
696
|
+
' """',
|
|
697
|
+
' Attempt to transition to a new state.',
|
|
698
|
+
' ',
|
|
699
|
+
' Args:',
|
|
700
|
+
' event: Event triggering the transition',
|
|
701
|
+
' ',
|
|
702
|
+
' Returns:',
|
|
703
|
+
' True if transition succeeded',
|
|
704
|
+
' """',
|
|
705
|
+
' key = (self.state, event)',
|
|
706
|
+
' if key in self._transitions:',
|
|
707
|
+
' old_state = self.state',
|
|
708
|
+
' self.state = self._transitions[key]',
|
|
709
|
+
' logger.info(f"State transition: {old_state} -> {self.state}")',
|
|
710
|
+
' return True',
|
|
711
|
+
' else:',
|
|
712
|
+
' logger.warning(f"Invalid transition: {self.state} + {event}")',
|
|
713
|
+
' return False',
|
|
714
|
+
'',
|
|
715
|
+
' def reset(self) -> None:',
|
|
716
|
+
' """Reset state machine to initial state."""',
|
|
717
|
+
f' self.state = {self.class_name}State.IDLE',
|
|
718
|
+
' self.error = None',
|
|
719
|
+
'',
|
|
720
|
+
' @property',
|
|
721
|
+
' def is_complete(self) -> bool:',
|
|
722
|
+
' """Check if execution is complete."""',
|
|
723
|
+
f' return self.state in ({self.class_name}State.COMPLETED, {self.class_name}State.FAILED)',
|
|
724
|
+
'',
|
|
725
|
+
' @property',
|
|
726
|
+
' def is_success(self) -> bool:',
|
|
727
|
+
' """Check if execution succeeded."""',
|
|
728
|
+
f' return self.state == {self.class_name}State.COMPLETED',
|
|
729
|
+
])
|
|
730
|
+
|
|
731
|
+
return '\n'.join(lines)
|
|
732
|
+
|
|
733
|
+
def generate(self, output_dir: Path) -> Dict[str, str]:
|
|
734
|
+
"""
|
|
735
|
+
Generate all skill code files.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
output_dir: Directory to write files to
|
|
739
|
+
|
|
740
|
+
Returns:
|
|
741
|
+
Dict mapping filenames to generated content
|
|
742
|
+
"""
|
|
743
|
+
output_dir = Path(output_dir)
|
|
744
|
+
skill_dir = output_dir / self.module_name
|
|
745
|
+
|
|
746
|
+
files = {
|
|
747
|
+
'skill.py': self.generate_skill_class(),
|
|
748
|
+
'primitives.py': self.generate_primitives_wrapper(),
|
|
749
|
+
'validators.py': self.generate_validators(),
|
|
750
|
+
'state_machine.py': self.generate_state_machine(),
|
|
751
|
+
'__init__.py': self._generate_init(),
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
# Create directory and write files
|
|
755
|
+
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
756
|
+
for filename, content in files.items():
|
|
757
|
+
(skill_dir / filename).write_text(content)
|
|
758
|
+
|
|
759
|
+
return files
|
|
760
|
+
|
|
761
|
+
def _generate_init(self) -> str:
|
|
762
|
+
"""Generate __init__.py for the skill package."""
|
|
763
|
+
return f'''"""
|
|
764
|
+
{self.spec.name} skill package.
|
|
765
|
+
|
|
766
|
+
{self.spec.description}
|
|
767
|
+
"""
|
|
768
|
+
|
|
769
|
+
from .skill import {self.class_name}Skill, {self.class_name}Input, {self.class_name}Output
|
|
770
|
+
from .primitives import PrimitiveExecutor
|
|
771
|
+
from .state_machine import {self.class_name}State, {self.class_name}StateMachine
|
|
772
|
+
|
|
773
|
+
__all__ = [
|
|
774
|
+
"{self.class_name}Skill",
|
|
775
|
+
"{self.class_name}Input",
|
|
776
|
+
"{self.class_name}Output",
|
|
777
|
+
"PrimitiveExecutor",
|
|
778
|
+
"{self.class_name}State",
|
|
779
|
+
"{self.class_name}StateMachine",
|
|
780
|
+
]
|
|
781
|
+
|
|
782
|
+
__version__ = "{self.spec.version}"
|
|
783
|
+
'''
|