330 lines
9.7 KiB
Python
330 lines
9.7 KiB
Python
#!/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()
|