#!/usr/bin/env python3 """ meta.agent - Meta-agent that creates other agents Transforms natural language descriptions into complete, functional agents by composing skills and generating proper artifact metadata. """ import json import yaml import sys import os from pathlib import Path from typing import Dict, List, Any, Optional # Add parent directory to path for imports parent_dir = str(Path(__file__).parent.parent.parent) sys.path.insert(0, parent_dir) # Import skill modules directly agent_compose_path = Path(parent_dir) / "skills" / "agent.compose" artifact_define_path = Path(parent_dir) / "skills" / "artifact.define" sys.path.insert(0, str(agent_compose_path)) sys.path.insert(0, str(artifact_define_path)) import agent_compose import artifact_define # Import traceability system from betty.traceability import get_tracer, RequirementInfo class AgentCreator: """Creates agents from natural language descriptions""" def __init__(self, registry_path: str = "registry/skills.json"): """Initialize with registry path""" self.registry_path = Path(registry_path) self.registry = self._load_registry() def _load_registry(self) -> Dict[str, Any]: """Load skills registry""" if not self.registry_path.exists(): raise FileNotFoundError(f"Registry not found: {self.registry_path}") with open(self.registry_path) as f: return json.load(f) def parse_description(self, description_path: str) -> Dict[str, Any]: """ Parse agent description from Markdown or JSON file Args: description_path: Path to agent_description.md or agent_description.json Returns: Parsed description with name, purpose, inputs, outputs, constraints """ 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": [], "constraints": {}, "examples": [] } current_section = None for line in content.split('\n'): line = line.strip() # Section headers if line.startswith('# Name:'): description["name"] = line.replace('# Name:', '').strip() elif line.startswith('# Purpose:'): current_section = "purpose" elif line.startswith('# Inputs:'): current_section = "inputs" elif line.startswith('# Outputs:'): current_section = "outputs" elif line.startswith('# Constraints:'): current_section = "constraints" elif line.startswith('# Examples:'): current_section = "examples" elif line and not line.startswith('#'): # Content for current section if current_section == "purpose": description["purpose"] += line + " " elif current_section == "inputs" and line.startswith('-'): # Extract artifact type (before parentheses or description) artifact = line[1:].strip() # Remove anything in parentheses and any extra description if '(' in artifact: artifact = artifact.split('(')[0].strip() description["inputs"].append(artifact) elif current_section == "outputs" and line.startswith('-'): # Extract artifact type (before parentheses or description) artifact = line[1:].strip() # Remove anything in parentheses and any extra description if '(' in artifact: artifact = artifact.split('(')[0].strip() description["outputs"].append(artifact) elif current_section == "examples" and line.startswith('-'): description["examples"].append(line[1:].strip()) description["purpose"] = description["purpose"].strip() return description def find_compatible_skills( self, purpose: str, required_artifacts: Optional[List[str]] = None ) -> Dict[str, Any]: """ Find compatible skills for agent purpose Args: purpose: Natural language description of agent purpose required_artifacts: List of artifact types the agent needs Returns: Dictionary with recommended skills and rationale """ return agent_compose.find_skills_for_purpose( self.registry, purpose, required_artifacts ) def generate_artifact_metadata( self, inputs: List[str], outputs: List[str] ) -> Dict[str, Any]: """ Generate artifact metadata from inputs/outputs Args: inputs: List of input artifact types outputs: List of output artifact types Returns: Artifact metadata structure """ metadata = {} if inputs: metadata["consumes"] = [] for input_type in inputs: artifact_def = artifact_define.get_artifact_definition(input_type) if artifact_def: metadata["consumes"].append(artifact_def) else: # Create basic definition metadata["consumes"].append({ "type": input_type, "description": f"Input artifact of type {input_type}" }) if outputs: metadata["produces"] = [] for output_type in outputs: artifact_def = artifact_define.get_artifact_definition(output_type) if artifact_def: metadata["produces"].append(artifact_def) else: # Create basic definition metadata["produces"].append({ "type": output_type, "description": f"Output artifact of type {output_type}" }) return metadata def infer_permissions(self, skills: List[str]) -> List[str]: """ Infer required permissions from skills Args: skills: List of skill names Returns: List of required permissions """ permissions = set() skills_list = self.registry.get("skills", []) for skill_name in skills: # Find skill in registry skill = next( (s for s in skills_list if s.get("name") == skill_name), None ) if skill and "permissions" in skill: for perm in skill["permissions"]: permissions.add(perm) return sorted(list(permissions)) def generate_agent_yaml( self, name: str, description: str, skills: List[str], artifact_metadata: Dict[str, Any], permissions: List[str], system_prompt: Optional[str] = None ) -> str: """ Generate agent.yaml content Args: name: Agent name description: Agent description skills: List of skill names artifact_metadata: Artifact metadata structure permissions: List of permissions system_prompt: Optional system prompt Returns: YAML content as string """ agent_def = { "name": name, "description": description, "skills_available": skills, "permissions": permissions } if artifact_metadata: agent_def["artifact_metadata"] = artifact_metadata if system_prompt: agent_def["system_prompt"] = system_prompt return yaml.dump( agent_def, default_flow_style=False, sort_keys=False, allow_unicode=True ) def generate_readme( self, name: str, purpose: str, skills: List[str], inputs: List[str], outputs: List[str], examples: List[str] ) -> str: """ Generate README.md content Args: name: Agent name purpose: Agent purpose skills: List of skill names inputs: Input artifacts outputs: Output artifacts examples: Example use cases Returns: Markdown content """ readme = f"""# {name.title()} Agent ## Purpose {purpose} ## Skills This agent uses the following skills: """ for skill in skills: readme += f"- `{skill}`\n" if inputs or outputs: readme += "\n## Artifact Flow\n\n" if inputs: readme += "### Consumes\n\n" for inp in inputs: readme += f"- `{inp}`\n" readme += "\n" if outputs: readme += "### Produces\n\n" for out in outputs: readme += f"- `{out}`\n" readme += "\n" if examples: readme += "## Example Use Cases\n\n" for example in examples: readme += f"- {example}\n" readme += "\n" readme += """## Usage ```bash # Activate the agent /agent {name} # Or invoke directly betty agent run {name} --input ``` ## Created By This agent was created by **meta.agent**, the meta-agent for creating agents. --- *Part of the Betty Framework* """.format(name=name) return readme def create_agent( self, description_path: str, output_dir: Optional[str] = None, validate: bool = True, requirement: Optional[RequirementInfo] = None ) -> Dict[str, str]: """ Create a complete agent from description Args: description_path: Path to agent description file output_dir: Output directory (default: agents/{name}/) validate: Whether to validate with registry.certify requirement: Requirement information for traceability (optional) Returns: Dictionary with paths to created files """ # Parse description desc = self.parse_description(description_path) name = desc["name"] if not name: raise ValueError("Agent name is required") # Determine output directory if not output_dir: output_dir = f"agents/{name}" output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) # Find compatible skills skill_recommendations = self.find_compatible_skills( desc["purpose"], desc.get("inputs", []) + desc.get("outputs", []) ) skills = skill_recommendations.get("recommended_skills", []) # Generate artifact metadata artifact_metadata = self.generate_artifact_metadata( desc.get("inputs", []), desc.get("outputs", []) ) # Infer permissions permissions = self.infer_permissions(skills) # Generate agent.yaml agent_yaml_content = self.generate_agent_yaml( name=name, description=desc["purpose"], skills=skills, artifact_metadata=artifact_metadata, permissions=permissions ) agent_yaml_path = output_path / "agent.yaml" with open(agent_yaml_path, 'w') as f: f.write(agent_yaml_content) # Generate README.md readme_content = self.generate_readme( name=name, purpose=desc["purpose"], skills=skills, inputs=desc.get("inputs", []), outputs=desc.get("outputs", []), examples=desc.get("examples", []) ) readme_path = output_path / "README.md" with open(readme_path, 'w') as f: f.write(readme_content) # Log traceability if requirement provided trace_id = None if requirement: try: tracer = get_tracer() trace_id = tracer.log_creation( component_id=name, component_name=name.replace(".", " ").title(), component_type="agent", component_version="0.1.0", component_file_path=str(agent_yaml_path), input_source_path=description_path, created_by_tool="meta.agent", created_by_version="0.1.0", requirement=requirement, tags=["agent", "auto-generated"], project="Betty Framework" ) # Log validation check tracer.log_verification( component_id=name, check_type="validation", tool="meta.agent", result="passed", details={ "checks_performed": [ {"name": "agent_structure", "status": "passed"}, {"name": "artifact_metadata", "status": "passed"}, {"name": "skills_compatibility", "status": "passed", "message": f"{len(skills)} compatible skills found"} ] } ) except Exception as e: print(f"āš ļø Warning: Could not log traceability: {e}") result = { "agent_yaml": str(agent_yaml_path), "readme": str(readme_path), "name": name, "skills": skills, "rationale": skill_recommendations.get("rationale", "") } if trace_id: result["trace_id"] = trace_id return result def main(): """CLI entry point""" import argparse parser = argparse.ArgumentParser( description="meta.agent - Create agents from natural language descriptions" ) parser.add_argument( "description", help="Path to agent description file (.md or .json)" ) parser.add_argument( "-o", "--output", help="Output directory (default: agents/{name}/)" ) parser.add_argument( "--no-validate", action="store_true", help="Skip validation step" ) # Traceability arguments parser.add_argument( "--requirement-id", help="Requirement identifier for traceability (e.g., REQ-2025-001)" ) parser.add_argument( "--requirement-description", help="What this agent is meant to accomplish" ) parser.add_argument( "--requirement-source", help="Source document or system (e.g., requirements/Q1-2025.md)" ) parser.add_argument( "--issue-id", help="Issue tracking ID (e.g., JIRA-123)" ) parser.add_argument( "--requested-by", help="Who requested this requirement" ) parser.add_argument( "--rationale", help="Why this component 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 ) # Create agent creator = AgentCreator() print(f"šŸ”® meta.agent creating agent from {args.description}...") try: result = creator.create_agent( args.description, output_dir=args.output, validate=not args.no_validate, requirement=requirement ) print(f"\n✨ Agent '{result['name']}' created successfully!\n") print(f"šŸ“„ Agent definition: {result['agent_yaml']}") print(f"šŸ“– Documentation: {result['readme']}\n") print(f"šŸ”§ Skills: {', '.join(result['skills'])}\n") if result.get("rationale"): print(f"šŸ’” Rationale:\n{result['rationale']}\n") if result.get("trace_id"): print(f"šŸ“ Traceability: {result['trace_id']}") print(f" View trace: python3 betty/trace_cli.py show {result['name']}\n") except Exception as e: print(f"\nāŒ Error creating agent: {e}", file=sys.stderr) sys.exit(1) if __name__ == "__main__": main()