#!/usr/bin/env python3 """ Ralph Agent Capability Registry Maintains a comprehensive registry of all available agents (contains-studio, custom, etc.) and their capabilities for dynamic selection and routing. """ import json import os import re from typing import Dict, List, Optional, Set from dataclasses import dataclass, field from enum import Enum import logging logger = logging.getLogger('ralph.registry') class AgentCategory(Enum): """Categories of agents""" ENGINEERING = "engineering" DESIGN = "design" PRODUCT = "product" MARKETING = "marketing" PROJECT_MANAGEMENT = "project-management" STUDIO_OPERATIONS = "studio-operations" TESTING = "testing" BONUS = "bonus" class TriggerType(Enum): """How agents can be triggered""" EXPLICIT = "explicit" # User mentions agent by name KEYWORD = "keyword" # Triggered by specific keywords CONTEXT = "context" # Triggered by project context PROACTIVE = "proactive" # Automatically triggers FILE_PATTERN = "file_pattern" # Triggered by file operations @dataclass class AgentCapability: """Represents a single agent's capabilities""" name: str category: AgentCategory description: str keywords: List[str] = field(default_factory=list) trigger_types: List[TriggerType] = field(default_factory=list) file_patterns: List[str] = field(default_factory=list) tools: List[str] = field(default_factory=list) examples: List[Dict] = field(default_factory=list) confidence_threshold: float = 0.5 priority: int = 5 # 1-10, higher = preferred class AgentCapabilityRegistry: """ Registry for all available agents and their capabilities Maintains: - Agent metadata and descriptions - Trigger keywords and patterns - Tool access requirements - Usage statistics - Performance metrics """ def __init__(self, agents_dir: Optional[str] = None): """Initialize the registry""" self.agents_dir = agents_dir or os.path.expanduser('~/.claude/agents') self.agents: Dict[str, AgentCapability] = {} self.keyword_index: Dict[str, Set[str]] = {} self.file_pattern_index: Dict[str, Set[str]] = {} self._load_agents() def _load_agents(self): """Load all agents from the agents directory""" logger.info(f"Loading agents from {self.agents_dir}") # Standard contains-studio structure categories = [ 'engineering', 'design', 'product', 'marketing', 'project-management', 'studio-operations', 'testing', 'bonus' ] for category in categories: category_path = os.path.join(self.agents_dir, category) if os.path.exists(category_path): self._load_category(category, category_path) # Also scan for individual .md files for root, dirs, files in os.walk(self.agents_dir): for file in files: if file.endswith('.md') and file != 'README.md': self._load_agent_file(os.path.join(root, file)) logger.info(f"Loaded {len(self.agents)} agents") def _load_category(self, category: str, category_path: str): """Load all agents from a category directory""" for file in os.listdir(category_path): if file.endswith('.md'): agent_path = os.path.join(category_path, file) self._load_agent_file(agent_path) def _load_agent_file(self, file_path: str): """Parse and load an agent definition from a markdown file""" try: with open(file_path, 'r') as f: content = f.read() # Parse YAML frontmatter frontmatter, body = self._parse_frontmatter(content) if not frontmatter.get('name'): return # Extract agent info name = frontmatter['name'] category = self._infer_category(file_path) description = frontmatter.get('description', '') # Extract keywords from description and examples keywords = self._extract_keywords(description) # Determine trigger types trigger_types = self._determine_trigger_types(frontmatter, body) # Extract file patterns file_patterns = self._extract_file_patterns(body) # Get tools tools = frontmatter.get('tools', []) if isinstance(tools, str): tools = [t.strip() for t in tools.split(',')] # Extract examples examples = self._extract_examples(body) # Create capability capability = AgentCapability( name=name, category=category, description=description, keywords=keywords, trigger_types=trigger_types, file_patterns=file_patterns, tools=tools, examples=examples, priority=self._calculate_priority(category, tools) ) self.agents[name] = capability # Build indexes for keyword in keywords: if keyword not in self.keyword_index: self.keyword_index[keyword] = set() self.keyword_index[keyword].add(name) for pattern in file_patterns: if pattern not in self.file_pattern_index: self.file_pattern_index[pattern] = set() self.file_pattern_index[pattern].add(name) except Exception as e: logger.warning(f"Error loading agent from {file_path}: {e}") def _parse_frontmatter(self, content: str) -> tuple: """Parse YAML frontmatter from markdown""" if not content.startswith('---'): return {}, content # Find end of frontmatter end = content.find('---', 4) if end == -1: return {}, content frontmatter_str = content[4:end].strip() body = content[end + 4:].strip() # Simple YAML parsing frontmatter = {} for line in frontmatter_str.split('\n'): if ':' in line: key, value = line.split(':', 1) frontmatter[key.strip()] = value.strip() return frontmatter, body def _infer_category(self, file_path: str) -> AgentCategory: """Infer category from file path""" path_lower = file_path.lower() if 'engineering' in path_lower: return AgentCategory.ENGINEERING elif 'design' in path_lower: return AgentCategory.DESIGN elif 'product' in path_lower: return AgentCategory.PRODUCT elif 'marketing' in path_lower: return AgentCategory.MARKETING elif 'project-management' in path_lower or 'project' in path_lower: return AgentCategory.PROJECT_MANAGEMENT elif 'studio-operations' in path_lower or 'operations' in path_lower: return AgentCategory.STUDIO_OPERATIONS elif 'testing' in path_lower or 'test' in path_lower: return AgentCategory.TESTING else: return AgentCategory.BONUS def _extract_keywords(self, description: str) -> List[str]: """Extract keywords from description""" keywords = [] # Common tech keywords tech_keywords = [ 'ai', 'ml', 'api', 'backend', 'frontend', 'mobile', 'ios', 'android', 'react', 'vue', 'svelte', 'angular', 'typescript', 'javascript', 'python', 'rust', 'go', 'java', 'swift', 'kotlin', 'ui', 'ux', 'design', 'component', 'layout', 'style', 'test', 'testing', 'unit', 'integration', 'e2e', 'deploy', 'ci', 'cd', 'docker', 'kubernetes', 'database', 'sql', 'nosql', 'redis', 'postgres', 'auth', 'authentication', 'oauth', 'jwt', 'payment', 'stripe', 'billing', 'design', 'figma', 'mockup', 'prototype', 'marketing', 'seo', 'social', 'content', 'analytics', 'metrics', 'data', 'performance', 'optimization', 'speed', 'security', 'compliance', 'legal', 'documentation', 'docs', 'readme' ] description_lower = description.lower() # Extract mentioned tech keywords for keyword in tech_keywords: if keyword in description_lower: keywords.append(keyword) # Extract from examples example_keywords = re.findall(r'example>\nContext: ([^\n]+)', description_lower) keywords.extend(example_keywords) # Extract action verbs actions = ['build', 'create', 'design', 'implement', 'refactor', 'test', 'deploy', 'optimize', 'fix', 'add', 'integrate', 'setup'] for action in actions: if action in description_lower: keywords.append(action) return list(set(keywords)) def _determine_trigger_types(self, frontmatter: Dict, body: str) -> List[TriggerType]: """Determine how this agent can be triggered""" trigger_types = [TriggerType.EXPLICIT, TriggerType.KEYWORD] body_lower = body.lower() # Check for proactive triggers if 'proactively' in body_lower or 'trigger automatically' in body_lower: trigger_types.append(TriggerType.PROACTIVE) # Check for file pattern triggers if any(ext in body_lower for ext in ['.tsx', '.py', '.rs', '.go', '.java']): trigger_types.append(TriggerType.FILE_PATTERN) # Check for context triggers if 'context' in body_lower or 'when' in body_lower: trigger_types.append(TriggerType.CONTEXT) return trigger_types def _extract_file_patterns(self, body: str) -> List[str]: """Extract file patterns that trigger this agent""" patterns = [] # Common patterns extensions = re.findall(r'\.([a-z]+)', body) for ext in set(extensions): if len(ext) <= 5: # Reasonable file extension patterns.append(f'*.{ext}') # Path patterns paths = re.findall(r'([a-z-]+/)', body.lower()) for path in set(paths): if path in ['src/', 'components/', 'tests/', 'docs/', 'api/']: patterns.append(path) return patterns def _extract_examples(self, body: str) -> List[Dict]: """Extract usage examples""" examples = [] # Find example blocks example_blocks = re.findall(r'(.*?)', body, re.DOTALL) for block in example_blocks: context_match = re.search(r'Context: ([^\n]+)', block) user_match = re.search(r'user: "([^"]+)"', block) assistant_match = re.search(r'assistant: "([^"]+)"', block) if context_match and user_match: examples.append({ 'context': context_match.group(1), 'user_request': user_match.group(1), 'response': assistant_match.group(1) if assistant_match else '', 'full_block': block.strip() }) return examples def _calculate_priority(self, category: AgentCategory, tools: List[str]) -> int: """Calculate agent priority for selection""" priority = 5 # Engineering agents tend to be higher priority if category == AgentCategory.ENGINEERING: priority = 7 elif category == AgentCategory.DESIGN: priority = 6 elif category == AgentCategory.TESTING: priority = 8 # Testing is proactive # Boost agents with more tools priority += min(len(tools), 3) return min(priority, 10) def find_agents_by_keywords(self, text: str) -> List[tuple]: """Find agents matching keywords in text, sorted by relevance""" text_lower = text.lower() words = set(text_lower.split()) matches = [] for agent_name, agent in self.agents.items(): score = 0 # Check keyword matches for keyword in agent.keywords: if keyword.lower() in text_lower: score += 1 # Check examples for example in agent.examples: if example['user_request'].lower() in text_lower: score += 3 # Check direct name mention if agent_name.lower() in text_lower: score += 10 if score > 0: matches.append((agent_name, score, agent)) # Sort by score, then priority matches.sort(key=lambda x: (x[1], x[2].priority), reverse=True) return matches def find_agents_by_files(self, files: List[str]) -> List[tuple]: """Find agents that should handle specific file types""" matches = [] for file_path in files: file_lower = file_path.lower() for agent_name, agent in self.agents.items(): score = 0 # Check file patterns for pattern in agent.file_patterns: if pattern in file_lower: score += 1 if score > 0: matches.append((agent_name, score, agent)) matches.sort(key=lambda x: (x[1], x[2].priority), reverse=True) return matches def find_proactive_agents(self, context: Dict) -> List[str]: """Find agents that should trigger proactively""" proactive = [] for agent_name, agent in self.agents.items(): if TriggerType.PROACTIVE in agent.trigger_types: # Check context if self._check_proactive_context(agent, context): proactive.append(agent_name) return proactive def _check_proactive_context(self, agent: AgentCapability, context: Dict) -> bool: """Check if agent should trigger proactively in this context""" # Test-writer-fixer triggers after code changes if agent.name == 'test-writer-fixer': return context.get('code_modified', False) # Whimsy-injector triggers after UI changes if agent.name == 'whimsy-injector': return context.get('ui_modified', False) # Studio-coach triggers on complex tasks if agent.name == 'studio-coach': return context.get('complexity', 0) > 7 return False def get_agent(self, name: str) -> Optional[AgentCapability]: """Get agent by name""" return self.agents.get(name) def get_all_agents(self) -> Dict[str, AgentCapability]: """Get all registered agents""" return self.agents def get_agents_by_category(self, category: AgentCategory) -> List[AgentCapability]: """Get all agents in a category""" return [a for a in self.agents.values() if a.category == category] # Pre-configured agent mappings for contains-studio agents CONTAINS_STUDIO_AGENTS = { # Engineering 'ai-engineer': { 'keywords': ['ai', 'ml', 'llm', 'machine learning', 'recommendation', 'chatbot', 'computer vision'], 'triggers': ['implement ai', 'add ml', 'integrate llm', 'build recommendation'] }, 'backend-architect': { 'keywords': ['api', 'backend', 'server', 'database', 'microservices'], 'triggers': ['design api', 'build backend', 'create database schema'] }, 'devops-automator': { 'keywords': ['deploy', 'ci/cd', 'docker', 'kubernetes', 'infrastructure'], 'triggers': ['set up deployment', 'configure ci', 'deploy to production'] }, 'frontend-developer': { 'keywords': ['frontend', 'ui', 'component', 'react', 'vue', 'svelte'], 'triggers': ['build component', 'create ui', 'implement frontend'] }, 'mobile-app-builder': { 'keywords': ['mobile', 'ios', 'android', 'react native', 'swift', 'kotlin'], 'triggers': ['build mobile app', 'create ios', 'develop android'] }, 'rapid-prototyper': { 'keywords': ['mvp', 'prototype', 'quick', 'scaffold', 'new app'], 'triggers': ['create prototype', 'build mvp', 'scaffold project', 'new app idea'] }, 'test-writer-fixer': { 'keywords': ['test', 'testing', 'coverage'], 'triggers': ['write tests', 'add coverage', 'test this'], 'proactive': True }, # Design 'brand-guardian': { 'keywords': ['brand', 'logo', 'identity', 'guidelines'], 'triggers': ['design brand', 'create logo', 'brand guidelines'] }, 'ui-designer': { 'keywords': ['ui', 'interface', 'design', 'component design'], 'triggers': ['design ui', 'create interface', 'ui design'] }, 'ux-researcher': { 'keywords': ['ux', 'user research', 'usability', 'user experience'], 'triggers': ['user research', 'ux study', 'usability test'] }, 'visual-storyteller': { 'keywords': ['visual', 'story', 'graphic', 'illustration'], 'triggers': ['create visual', 'design graphics', 'story telling'] }, 'whimsy-injector': { 'keywords': ['delight', 'surprise', 'fun', 'animation'], 'triggers': ['add delight', 'make fun', 'surprise users'], 'proactive': True }, # Product 'feedback-synthesizer': { 'keywords': ['feedback', 'reviews', 'complaints', 'user input'], 'triggers': ['analyze feedback', 'synthesize reviews', 'user complaints'] }, 'sprint-prioritizer': { 'keywords': ['sprint', 'priority', 'roadmap', 'planning'], 'triggers': ['plan sprint', 'prioritize features', 'sprint planning'] }, 'trend-researcher': { 'keywords': ['trend', 'viral', 'market research', 'opportunity'], 'triggers': ['research trends', 'whats trending', 'market analysis'] }, # Marketing 'app-store-optimizer': { 'keywords': ['app store', 'aso', 'store listing', 'keywords'], 'triggers': ['optimize app store', 'improve aso', 'store listing'] }, 'content-creator': { 'keywords': ['content', 'blog', 'social media', 'copy'], 'triggers': ['create content', 'write blog', 'social content'] }, 'growth-hacker': { 'keywords': ['growth', 'viral', 'acquisition', 'funnel'], 'triggers': ['growth strategy', 'viral loop', 'acquisition'] }, 'tiktok-strategist': { 'keywords': ['tiktok', 'video', 'viral', 'content'], 'triggers': ['tiktok strategy', 'viral video', 'tiktok content'] }, # Project Management 'experiment-tracker': { 'keywords': ['experiment', 'a/b test', 'feature flag'], 'triggers': ['track experiment', 'a/b testing', 'feature flags'], 'proactive': True }, 'project-shipper': { 'keywords': ['launch', 'ship', 'release', 'deploy'], 'triggers': ['prepare launch', 'ship project', 'release management'] }, 'studio-producer': { 'keywords': ['coordinate', 'team', 'workflow', 'manage'], 'triggers': ['coordinate team', 'manage project', 'workflow'] }, # Testing 'api-tester': { 'keywords': ['api test', 'load test', 'endpoint testing'], 'triggers': ['test api', 'load testing', 'endpoint test'] }, 'performance-benchmarker': { 'keywords': ['performance', 'benchmark', 'speed', 'optimization'], 'triggers': ['benchmark performance', 'speed test', 'optimize'] }, 'test-results-analyzer': { 'keywords': ['test results', 'analyze tests', 'test failures'], 'triggers': ['analyze test results', 'test failures', 'test report'] }, # Studio Operations 'analytics-reporter': { 'keywords': ['analytics', 'metrics', 'data', 'reports'], 'triggers': ['generate report', 'analyze metrics', 'analytics'] }, 'finance-tracker': { 'keywords': ['finance', 'budget', 'costs', 'revenue'], 'triggers': ['track costs', 'budget analysis', 'financial report'] }, 'infrastructure-maintainer': { 'keywords': ['infrastructure', 'servers', 'monitoring', 'uptime'], 'triggers': ['check infrastructure', 'server health', 'monitoring'] }, 'support-responder': { 'keywords': ['support', 'help', 'customer service'], 'triggers': ['handle support', 'customer inquiry', 'help ticket'] }, # Bonus 'joker': { 'keywords': ['joke', 'humor', 'funny', 'laugh'], 'triggers': ['tell joke', 'add humor', 'make funny'] }, 'studio-coach': { 'keywords': ['coach', 'guidance', 'help', 'advice'], 'triggers': ['need help', 'guidance', 'coach'], 'proactive': True } }