#!/usr/bin/env python3 """ agent_compose.py - Recommend skills for Betty agents based on purpose Analyzes skill artifact metadata to suggest compatible skill combinations. """ import os import sys import json import yaml from typing import Dict, Any, List, Optional, Set from pathlib import Path from betty.config import BASE_DIR from betty.logging_utils import setup_logger logger = setup_logger(__name__) def load_registry() -> Dict[str, Any]: """Load skills registry.""" registry_path = os.path.join(BASE_DIR, "registry", "skills.json") with open(registry_path) as f: return json.load(f) def extract_artifact_metadata(skill: Dict[str, Any]) -> Dict[str, Any]: """ Extract artifact metadata from a skill. Returns: Dict with 'produces' and 'consumes' sets """ metadata = skill.get("artifact_metadata", {}) return { "produces": set(a.get("type") for a in metadata.get("produces", [])), "consumes": set(a.get("type") for a in metadata.get("consumes", [])) } def find_skills_by_artifacts( registry: Dict[str, Any], produces: Optional[List[str]] = None, consumes: Optional[List[str]] = None ) -> List[Dict[str, Any]]: """ Find skills that produce or consume specific artifacts. Args: registry: Skills registry produces: Artifact types to produce consumes: Artifact types to consume Returns: List of matching skills with metadata """ skills = registry.get("skills", []) matches = [] for skill in skills: if skill.get("status") != "active": continue artifacts = extract_artifact_metadata(skill) # Check if skill produces required artifacts produces_match = not produces or any( artifact in artifacts["produces"] for artifact in produces ) # Check if skill consumes specified artifacts consumes_match = not consumes or any( artifact in artifacts["consumes"] for artifact in consumes ) if produces_match or consumes_match: matches.append({ "name": skill["name"], "description": skill.get("description", ""), "produces": list(artifacts["produces"]), "consumes": list(artifacts["consumes"]), "tags": skill.get("tags", []) }) return matches def find_skills_for_purpose( registry: Dict[str, Any], purpose: str, required_artifacts: Optional[List[str]] = None ) -> Dict[str, Any]: """ Find skills for agent purpose (alias for recommend_skills_for_purpose). Args: registry: Skills registry (for compatibility, currently unused) purpose: Description of agent purpose required_artifacts: Artifact types agent needs to work with Returns: Recommendation result with skills and rationale """ return recommend_skills_for_purpose(purpose, required_artifacts) def recommend_skills_for_purpose( agent_purpose: str, required_artifacts: Optional[List[str]] = None ) -> Dict[str, Any]: """ Recommend skills based on agent purpose and required artifacts. Args: agent_purpose: Description of agent purpose required_artifacts: Artifact types agent needs to work with Returns: Recommendation result with skills and rationale """ registry = load_registry() recommended = [] rationale = {} # Keyword matching for purpose purpose_lower = agent_purpose.lower() keywords = { "api": ["api.define", "api.validate", "api.generate-models", "api.compatibility"], "workflow": ["workflow.validate", "workflow.compose"], "hook": ["hook.define"], "validate": ["api.validate", "workflow.validate"], "design": ["api.define"], } # Find skills by keywords matched_by_keyword = set() for keyword, skill_names in keywords.items(): if keyword in purpose_lower: matched_by_keyword.update(skill_names) # Find skills by required artifacts matched_by_artifacts = set() if required_artifacts: artifact_skills = find_skills_by_artifacts( registry, produces=required_artifacts, consumes=required_artifacts ) matched_by_artifacts.update(s["name"] for s in artifact_skills) # Combine matches all_matches = matched_by_keyword | matched_by_artifacts # Build recommendation with rationale skills = registry.get("skills", []) for skill in skills: skill_name = skill.get("name") if skill_name in all_matches: reasons = [] if skill_name in matched_by_keyword: reasons.append(f"Purpose matches skill capabilities") artifacts = extract_artifact_metadata(skill) if required_artifacts: produces_match = artifacts["produces"] & set(required_artifacts) consumes_match = artifacts["consumes"] & set(required_artifacts) if produces_match: reasons.append(f"Produces: {', '.join(produces_match)}") if consumes_match: reasons.append(f"Consumes: {', '.join(consumes_match)}") recommended.append(skill_name) rationale[skill_name] = { "description": skill.get("description", ""), "reasons": reasons, "produces": list(artifacts["produces"]), "consumes": list(artifacts["consumes"]) } return { "recommended_skills": recommended, "rationale": rationale, "total_recommended": len(recommended) } def analyze_artifact_flow(skills_metadata: List[Dict[str, Any]]) -> Dict[str, Any]: """ Analyze artifact flow between recommended skills. Args: skills_metadata: List of skill metadata Returns: Flow analysis showing how artifacts move between skills """ all_produces = set() all_consumes = set() flows = [] for skill in skills_metadata: produces = set(skill.get("produces", [])) consumes = set(skill.get("consumes", [])) all_produces.update(produces) all_consumes.update(consumes) for artifact in produces: consumers = [ s["name"] for s in skills_metadata if artifact in s.get("consumes", []) ] if consumers: flows.append({ "artifact": artifact, "producer": skill["name"], "consumers": consumers }) # Find gaps (consumed but not produced) gaps = all_consumes - all_produces return { "flows": flows, "gaps": list(gaps), "fully_covered": len(gaps) == 0 } def main(): """CLI entry point.""" import argparse parser = argparse.ArgumentParser( description="Recommend skills for a Betty agent" ) parser.add_argument( "agent_purpose", help="Description of what the agent should do" ) parser.add_argument( "--required-artifacts", nargs="+", help="Artifact types the agent needs to work with" ) parser.add_argument( "--output-format", choices=["yaml", "json", "markdown"], default="yaml", help="Output format" ) args = parser.parse_args() logger.info(f"Finding skills for agent purpose: {args.agent_purpose}") try: # Get recommendations result = recommend_skills_for_purpose( args.agent_purpose, args.required_artifacts ) # Analyze artifact flow skills_metadata = list(result["rationale"].values()) for skill_name, metadata in result["rationale"].items(): metadata["name"] = skill_name flow_analysis = analyze_artifact_flow(skills_metadata) result["artifact_flow"] = flow_analysis # Format output if args.output_format == "yaml": print("\n# Recommended Skills for Agent\n") print(f"# Purpose: {args.agent_purpose}\n") print("skills_available:") for skill in result["recommended_skills"]: print(f" - {skill}") print("\n# Rationale:") for skill_name, rationale in result["rationale"].items(): print(f"\n# {skill_name}:") print(f"# {rationale['description']}") for reason in rationale["reasons"]: print(f"# - {reason}") elif args.output_format == "markdown": print(f"\n## Recommended Skills for: {args.agent_purpose}\n") print("### Skills\n") for skill in result["recommended_skills"]: rationale = result["rationale"][skill] print(f"**{skill}**") print(f"- {rationale['description']}") for reason in rationale["reasons"]: print(f" - {reason}") print() else: # json print(json.dumps(result, indent=2)) # Show warnings for gaps if flow_analysis["gaps"]: logger.warning(f"\n⚠️ Artifact gaps detected:") for gap in flow_analysis["gaps"]: logger.warning(f" - '{gap}' is consumed but not produced") logger.warning(" Consider adding skills that produce these artifacts") logger.info(f"\n✅ Recommended {result['total_recommended']} skills") sys.exit(0) except Exception as e: logger.error(f"Failed to compose agent: {e}") result = { "ok": False, "status": "failed", "error": str(e) } print(json.dumps(result, indent=2)) sys.exit(1) if __name__ == "__main__": main()