#!/usr/bin/env python3 """ meta.skill - Skill Creator Creates complete, functional skills from natural language descriptions. Generates skill.yaml, implementation stub, tests, and documentation. """ import json import yaml import sys import os import re from pathlib import Path from typing import Dict, List, Any, Optional from datetime import datetime # Add parent directory to path for imports parent_dir = str(Path(__file__).parent.parent.parent) sys.path.insert(0, parent_dir) from betty.traceability import get_tracer, RequirementInfo # Import artifact validation from artifact.define skill try: import importlib.util artifact_define_path = Path(__file__).parent.parent.parent / "skills" / "artifact.define" / "artifact_define.py" spec = importlib.util.spec_from_file_location("artifact_define", artifact_define_path) artifact_define_module = importlib.util.module_from_spec(spec) spec.loader.exec_module(artifact_define_module) validate_artifact_type = artifact_define_module.validate_artifact_type KNOWN_ARTIFACT_TYPES = artifact_define_module.KNOWN_ARTIFACT_TYPES ARTIFACT_VALIDATION_AVAILABLE = True except Exception as e: ARTIFACT_VALIDATION_AVAILABLE = False class SkillCreator: """Creates skills from natural language descriptions""" def __init__(self, base_dir: str = "."): """Initialize with base directory""" self.base_dir = Path(base_dir) self.skills_dir = self.base_dir / "skills" self.registry_path = self.base_dir / "registry" / "skills.json" def parse_description(self, description_path: str) -> Dict[str, Any]: """ Parse skill description from Markdown or JSON file Args: description_path: Path to skill_description.md or .json Returns: Parsed description with skill metadata """ path = Path(description_path) if not path.exists(): raise FileNotFoundError(f"Description not found: {description_path}") # Handle JSON format if path.suffix == ".json": with open(path) as f: return json.load(f) # Handle Markdown format with open(path) as f: content = f.read() # Parse Markdown sections description = { "name": "", "purpose": "", "inputs": [], "outputs": [], "permissions": [], "implementation_notes": "", "examples": [], "artifact_produces": [], "artifact_consumes": [] } current_section = None for line in content.split('\n'): line_stripped = line.strip() # Section headers if line_stripped.startswith('# Name:'): description["name"] = line_stripped.replace('# Name:', '').strip() elif line_stripped.startswith('# Purpose:'): current_section = "purpose" elif line_stripped.startswith('# Inputs:'): current_section = "inputs" elif line_stripped.startswith('# Outputs:'): current_section = "outputs" elif line_stripped.startswith('# Permissions:'): current_section = "permissions" elif line_stripped.startswith('# Implementation Notes:'): current_section = "implementation_notes" elif line_stripped.startswith('# Examples:'): current_section = "examples" elif line_stripped.startswith('# Produces Artifacts:'): current_section = "artifact_produces" elif line_stripped.startswith('# Consumes Artifacts:'): current_section = "artifact_consumes" elif line_stripped and not line_stripped.startswith('#'): # Content for current section if current_section == "purpose": description["purpose"] += line_stripped + " " elif current_section == "implementation_notes": description["implementation_notes"] += line_stripped + " " elif current_section in ["inputs", "outputs", "permissions", "examples", "artifact_produces", "artifact_consumes"] and line_stripped.startswith('-'): description[current_section].append(line_stripped[1:].strip()) description["purpose"] = description["purpose"].strip() description["implementation_notes"] = description["implementation_notes"].strip() return description def generate_skill_yaml(self, skill_desc: Dict[str, Any]) -> str: """ Generate skill.yaml content Args: skill_desc: Parsed skill description Returns: YAML content as string """ skill_name = skill_desc["name"] # Convert skill.name to skill_name format for handler handler_name = skill_name.replace('.', '_') + ".py" skill_def = { "name": skill_name, "version": "0.1.0", "description": skill_desc["purpose"], "inputs": skill_desc.get("inputs", []), "outputs": skill_desc.get("outputs", []), "status": "active", "permissions": skill_desc.get("permissions", ["filesystem:read"]), "entrypoints": [ { "command": f"/{skill_name.replace('.', '/')}", "handler": handler_name, "runtime": "python", "description": skill_desc["purpose"][:100] } ] } # Add artifact metadata if specified if skill_desc.get("artifact_produces") or skill_desc.get("artifact_consumes"): artifact_metadata = {} if skill_desc.get("artifact_produces"): artifact_metadata["produces"] = [ {"type": art_type} for art_type in skill_desc["artifact_produces"] ] if skill_desc.get("artifact_consumes"): artifact_metadata["consumes"] = [ {"type": art_type, "required": True} for art_type in skill_desc["artifact_consumes"] ] skill_def["artifact_metadata"] = artifact_metadata return yaml.dump(skill_def, default_flow_style=False, sort_keys=False) def generate_implementation(self, skill_desc: Dict[str, Any]) -> str: """ Generate Python implementation stub Args: skill_desc: Parsed skill description Returns: Python code as string """ skill_name = skill_desc["name"] module_name = skill_name.replace('.', '_') class_name = ''.join(word.capitalize() for word in skill_name.split('.')) implementation = f'''#!/usr/bin/env python3 """ {skill_name} - {skill_desc["purpose"]} Generated by meta.skill with Betty Framework certification """ import os import sys import json import yaml from pathlib import Path from typing import Dict, List, Any, Optional # Add parent directory to path for imports sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) from betty.config import BASE_DIR from betty.logging_utils import setup_logger from betty.certification import certified_skill logger = setup_logger(__name__) class {class_name}: """ {skill_desc["purpose"]} """ def __init__(self, base_dir: str = BASE_DIR): """Initialize skill""" self.base_dir = Path(base_dir) @certified_skill("{skill_name}") def execute(self''' # Add input parameters if skill_desc.get("inputs"): for inp in skill_desc["inputs"]: # Sanitize parameter names - remove special characters, keep only alphanumeric and underscores param_name = ''.join(c if c.isalnum() or c in ' -_' else '' for c in inp.lower()) param_name = param_name.replace(' ', '_').replace('-', '_') implementation += f', {param_name}: Optional[str] = None' implementation += f''') -> Dict[str, Any]: """ Execute the skill Returns: Dict with execution results """ try: logger.info("Executing {skill_name}...") # TODO: Implement skill logic here ''' if skill_desc.get("implementation_notes"): implementation += f''' # Implementation notes: # {skill_desc["implementation_notes"]} ''' # Escape the purpose string for Python string literal escaped_purpose = skill_desc['purpose'].replace('"', '\\"') implementation += f''' # Placeholder implementation result = {{ "ok": True, "status": "success", "message": "Skill executed successfully" }} logger.info("Skill completed successfully") return result except Exception as e: logger.error(f"Error executing skill: {{e}}") return {{ "ok": False, "status": "failed", "error": str(e) }} def main(): """CLI entry point""" import argparse parser = argparse.ArgumentParser( description="{escaped_purpose}" ) ''' # Add CLI arguments for inputs if skill_desc.get("inputs"): for inp in skill_desc["inputs"]: # Sanitize parameter names - remove special characters param_name = ''.join(c if c.isalnum() or c in ' -_' else '' for c in inp.lower()) param_name = param_name.replace(' ', '_').replace('-', '_') implementation += f''' parser.add_argument( "--{param_name.replace('_', '-')}", help="{inp}" )''' implementation += f''' parser.add_argument( "--output-format", choices=["json", "yaml"], default="json", help="Output format" ) args = parser.parse_args() # Create skill instance skill = {class_name}() # Execute skill result = skill.execute(''' if skill_desc.get("inputs"): for inp in skill_desc["inputs"]: # Sanitize parameter names - remove special characters param_name = ''.join(c if c.isalnum() or c in ' -_' else '' for c in inp.lower()) param_name = param_name.replace(' ', '_').replace('-', '_') implementation += f''' {param_name}=args.{param_name},''' implementation += ''' ) # Output result if args.output_format == "json": print(json.dumps(result, indent=2)) else: print(yaml.dump(result, default_flow_style=False)) # Exit with appropriate code sys.exit(0 if result.get("ok") else 1) if __name__ == "__main__": main() ''' return implementation def generate_tests(self, skill_desc: Dict[str, Any]) -> str: """ Generate test template Args: skill_desc: Parsed skill description Returns: Python test code as string """ skill_name = skill_desc["name"] module_name = skill_name.replace('.', '_') class_name = ''.join(word.capitalize() for word in skill_name.split('.')) tests = f'''#!/usr/bin/env python3 """ Tests for {skill_name} Generated by meta.skill """ import pytest import sys import os from pathlib import Path # Add parent directory to path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) from skills.{skill_name.replace('.', '_')} import {module_name} class Test{class_name}: """Tests for {class_name}""" def setup_method(self): """Setup test fixtures""" self.skill = {module_name}.{class_name}() def test_initialization(self): """Test skill initializes correctly""" assert self.skill is not None assert self.skill.base_dir is not None def test_execute_basic(self): """Test basic execution""" result = self.skill.execute() assert result is not None assert "ok" in result assert "status" in result def test_execute_success(self): """Test successful execution""" result = self.skill.execute() assert result["ok"] is True assert result["status"] == "success" # TODO: Add more specific tests based on skill functionality def test_cli_help(capsys): """Test CLI help message""" sys.argv = ["{module_name}.py", "--help"] with pytest.raises(SystemExit) as exc_info: {module_name}.main() assert exc_info.value.code == 0 captured = capsys.readouterr() assert "{skill_desc['purpose'][:50]}" in captured.out if __name__ == "__main__": pytest.main([__file__, "-v"]) ''' return tests def generate_skill_md(self, skill_desc: Dict[str, Any]) -> str: """ Generate SKILL.md Args: skill_desc: Parsed skill description Returns: Markdown content as string """ skill_name = skill_desc["name"] readme = f'''# {skill_name} {skill_desc["purpose"]} ## Overview **Purpose:** {skill_desc["purpose"]} **Command:** `/{skill_name.replace('.', '/')}` ## Usage ### Basic Usage ```bash python3 skills/{skill_name.replace('.', '/')}/{skill_name.replace('.', '_')}.py ``` ### With Arguments ```bash python3 skills/{skill_name.replace('.', '/')}/{skill_name.replace('.', '_')}.py \\ ''' if skill_desc.get("inputs"): for inp in skill_desc["inputs"]: param_name = inp.lower().replace(' ', '_').replace('-', '-') readme += f' --{param_name} "value" \\\n' readme += ' --output-format json\n```\n\n' if skill_desc.get("inputs"): readme += "## Inputs\n\n" for inp in skill_desc["inputs"]: readme += f"- **{inp}**\n" readme += "\n" if skill_desc.get("outputs"): readme += "## Outputs\n\n" for out in skill_desc["outputs"]: readme += f"- **{out}**\n" readme += "\n" if skill_desc.get("artifact_consumes") or skill_desc.get("artifact_produces"): readme += "## Artifact Metadata\n\n" if skill_desc.get("artifact_consumes"): readme += "### Consumes\n\n" for art in skill_desc["artifact_consumes"]: readme += f"- `{art}`\n" readme += "\n" if skill_desc.get("artifact_produces"): readme += "### Produces\n\n" for art in skill_desc["artifact_produces"]: readme += f"- `{art}`\n" readme += "\n" if skill_desc.get("examples"): readme += "## Examples\n\n" for example in skill_desc["examples"]: readme += f"- {example}\n" readme += "\n" if skill_desc.get("permissions"): readme += "## Permissions\n\n" for perm in skill_desc["permissions"]: readme += f"- `{perm}`\n" readme += "\n" if skill_desc.get("implementation_notes"): readme += "## Implementation Notes\n\n" readme += f"{skill_desc['implementation_notes']}\n\n" readme += f'''## Integration This skill can be used in agents by including it in `skills_available`: ```yaml name: my.agent skills_available: - {skill_name} ``` ## Testing Run tests with: ```bash pytest skills/{skill_name.replace('.', '/')}/test_{skill_name.replace('.', '_')}.py -v ``` ## Created By This skill was generated by **meta.skill**, the skill creator meta-agent. --- *Part of the Betty Framework* ''' return readme def validate_artifacts(self, skill_desc: Dict[str, Any]) -> List[str]: """ Validate that artifact types exist in the known registry. Args: skill_desc: Parsed skill description Returns: List of warning messages """ warnings = [] if not ARTIFACT_VALIDATION_AVAILABLE: warnings.append( "Artifact validation skipped: artifact.define skill not available" ) return warnings # Validate produced artifacts for artifact_type in skill_desc.get("artifact_produces", []): is_valid, warning = validate_artifact_type(artifact_type) if not is_valid and warning: warnings.append(f"Produces: {warning}") # Validate consumed artifacts for artifact_type in skill_desc.get("artifact_consumes", []): is_valid, warning = validate_artifact_type(artifact_type) if not is_valid and warning: warnings.append(f"Consumes: {warning}") return warnings def create_skill( self, description_path: str, output_dir: Optional[str] = None, requirement: Optional[RequirementInfo] = None ) -> Dict[str, Any]: """ Create a complete skill from description Args: description_path: Path to skill description file output_dir: Output directory (default: skills/{name}/) requirement: Optional requirement information for traceability Returns: Summary of created files """ # Parse description skill_desc = self.parse_description(description_path) skill_name = skill_desc["name"] if not skill_name: raise ValueError("Skill name is required") # Validate name format (domain.action) if not re.match(r'^[a-z0-9]+\.[a-z0-9]+$', skill_name): raise ValueError( f"Skill name must be in domain.action format: {skill_name}" ) # Validate artifact types artifact_warnings = self.validate_artifacts(skill_desc) if artifact_warnings: print("\n⚠️ Artifact Validation Warnings:") for warning in artifact_warnings: print(f" {warning}") print() # Determine output directory if not output_dir: output_dir = f"skills/{skill_name}" output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) result = { "skill_name": skill_name, "created_files": [], "errors": [], "artifact_warnings": artifact_warnings } # Generate and save skill.yaml skill_yaml_content = self.generate_skill_yaml(skill_desc) skill_yaml_path = output_path / "skill.yaml" with open(skill_yaml_path, 'w') as f: f.write(skill_yaml_content) result["created_files"].append(str(skill_yaml_path)) # Generate and save implementation impl_content = self.generate_implementation(skill_desc) impl_path = output_path / f"{skill_name.replace('.', '_')}.py" with open(impl_path, 'w') as f: f.write(impl_content) os.chmod(impl_path, 0o755) # Make executable result["created_files"].append(str(impl_path)) # Generate and save tests tests_content = self.generate_tests(skill_desc) tests_path = output_path / f"test_{skill_name.replace('.', '_')}.py" with open(tests_path, 'w') as f: f.write(tests_content) result["created_files"].append(str(tests_path)) # Generate and save SKILL.md skill_md_content = self.generate_skill_md(skill_desc) skill_md_path = output_path / "SKILL.md" with open(skill_md_path, 'w') as f: f.write(skill_md_content) result["created_files"].append(str(skill_md_path)) # Log traceability if requirement provided trace_id = None if requirement: try: tracer = get_tracer() trace_id = tracer.log_creation( component_id=skill_name, component_name=skill_name.replace(".", " ").title(), component_type="skill", component_version="0.1.0", component_file_path=str(skill_yaml_path), input_source_path=description_path, created_by_tool="meta.skill", created_by_version="0.1.0", requirement=requirement, tags=["skill", "auto-generated"], project="Betty Framework" ) # Log validation check validation_details = { "checks_performed": [ {"name": "skill_structure", "status": "passed"}, {"name": "artifact_metadata", "status": "passed"} ] } # Check for artifact metadata if skill_desc.get("artifact_produces") or skill_desc.get("artifact_consumes"): validation_details["checks_performed"].append({ "name": "artifact_metadata_completeness", "status": "passed", "message": f"Produces: {len(skill_desc.get('artifact_produces', []))}, Consumes: {len(skill_desc.get('artifact_consumes', []))}" }) tracer.log_verification( component_id=skill_name, check_type="validation", tool="meta.skill", result="passed", details=validation_details ) result["trace_id"] = trace_id except Exception as e: print(f"⚠️ Warning: Could not log traceability: {e}") return result def main(): """CLI entry point""" import argparse parser = argparse.ArgumentParser( description="meta.skill - Create skills from descriptions" ) parser.add_argument( "description", help="Path to skill description file (.md or .json)" ) parser.add_argument( "-o", "--output", help="Output directory (default: skills/{name}/)" ) # Traceability arguments parser.add_argument( "--requirement-id", help="Requirement identifier (e.g., REQ-2025-001)" ) parser.add_argument( "--requirement-description", help="What this skill accomplishes" ) parser.add_argument( "--requirement-source", help="Source document" ) parser.add_argument( "--issue-id", help="Issue tracking ID (e.g., JIRA-123)" ) parser.add_argument( "--requested-by", help="Who requested this" ) parser.add_argument( "--rationale", help="Why this is needed" ) args = parser.parse_args() # Create requirement info if provided requirement = None if args.requirement_id and args.requirement_description: requirement = RequirementInfo( id=args.requirement_id, description=args.requirement_description, source=args.requirement_source, issue_id=args.issue_id, requested_by=args.requested_by, rationale=args.rationale ) creator = SkillCreator() print(f"🛠️ meta.skill - Creating skill from {args.description}") try: result = creator.create_skill( args.description, output_dir=args.output, requirement=requirement ) print(f"\n✨ Skill '{result['skill_name']}' created successfully!\n") if result["created_files"]: print("📄 Created files:") for file in result["created_files"]: print(f" - {file}") if result["errors"]: print("\n⚠️ Warnings:") for error in result["errors"]: print(f" - {error}") if result.get("trace_id"): print(f"\n📝 Traceability: {result['trace_id']}") print(f" View trace: python3 betty/trace_cli.py show {result['skill_name']}") print(f"\n✅ Skill '{result['skill_name']}' is ready to use") print(" Add to agent skills_available to use it.") except Exception as e: print(f"\n❌ Error creating skill: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()