This commit is contained in:
Pablo Estevez
2026-01-17 17:29:21 +00:00
parent c89f059712
commit 5ed767ff9a
144 changed files with 14142 additions and 16488 deletions

View File

@@ -12,17 +12,17 @@ Phase 4 enhancements:
- GitHub issue links for context
"""
import argparse
import json
import sys
import argparse
from pathlib import Path
from typing import Dict, List, Any, Tuple, Optional
from typing import Any, Optional
# Import three-stream data classes (Phase 1)
try:
from .github_fetcher import ThreeStreamData, DocsStream, InsightsStream
from .merge_sources import categorize_issues_by_topic
from .github_fetcher import DocsStream, InsightsStream, ThreeStreamData
from .markdown_cleaner import MarkdownCleaner
from .merge_sources import categorize_issues_by_topic
except ImportError:
# Fallback if github_fetcher not available
ThreeStreamData = None
@@ -34,10 +34,9 @@ except ImportError:
class RouterGenerator:
"""Generates router skills that direct to specialized sub-skills with GitHub integration"""
def __init__(self,
config_paths: List[str],
router_name: str = None,
github_streams: Optional['ThreeStreamData'] = None):
def __init__(
self, config_paths: list[str], router_name: str = None, github_streams: Optional["ThreeStreamData"] = None
):
"""
Initialize router generator with optional GitHub streams.
@@ -60,21 +59,21 @@ class RouterGenerator:
if github_streams and github_streams.insights_stream:
self.github_metadata = github_streams.insights_stream.metadata
self.github_issues = {
'common_problems': github_streams.insights_stream.common_problems,
'known_solutions': github_streams.insights_stream.known_solutions,
'top_labels': github_streams.insights_stream.top_labels
"common_problems": github_streams.insights_stream.common_problems,
"known_solutions": github_streams.insights_stream.known_solutions,
"top_labels": github_streams.insights_stream.top_labels,
}
if github_streams and github_streams.docs_stream:
self.github_docs = {
'readme': github_streams.docs_stream.readme,
'contributing': github_streams.docs_stream.contributing
"readme": github_streams.docs_stream.readme,
"contributing": github_streams.docs_stream.contributing,
}
def load_config(self, path: Path) -> Dict[str, Any]:
def load_config(self, path: Path) -> dict[str, Any]:
"""Load a config file"""
try:
with open(path, 'r') as f:
with open(path) as f:
return json.load(f)
except Exception as e:
print(f"❌ Error loading {path}: {e}")
@@ -83,17 +82,17 @@ class RouterGenerator:
def infer_router_name(self) -> str:
"""Infer router name from sub-skill names"""
# Find common prefix
names = [cfg['name'] for cfg in self.configs]
names = [cfg["name"] for cfg in self.configs]
if not names:
return "router"
# Get common prefix before first dash
first_name = names[0]
if '-' in first_name:
return first_name.split('-')[0]
if "-" in first_name:
return first_name.split("-")[0]
return first_name
def extract_routing_keywords(self) -> Dict[str, List[str]]:
def extract_routing_keywords(self) -> dict[str, list[str]]:
"""
Extract keywords for routing to each skill (Phase 4 enhanced).
@@ -103,26 +102,26 @@ class RouterGenerator:
routing = {}
for config in self.configs:
name = config['name']
name = config["name"]
keywords = []
# Extract from categories (base weight: 1x)
if 'categories' in config:
keywords.extend(config['categories'].keys())
if "categories" in config:
keywords.extend(config["categories"].keys())
# Extract from name (part after dash)
if '-' in name:
skill_topic = name.split('-', 1)[1]
if "-" in name:
skill_topic = name.split("-", 1)[1]
keywords.append(skill_topic)
# Phase 4: Add GitHub issue labels (weight 2x by including twice)
if self.github_issues:
# Get top labels related to this skill topic
top_labels = self.github_issues.get('top_labels', [])
top_labels = self.github_issues.get("top_labels", [])
skill_keywords = set(keywords)
for label_info in top_labels[:10]: # Top 10 labels
label = label_info['label'].lower()
label = label_info["label"].lower()
# Check if label relates to any skill keyword
if any(keyword.lower() in label or label in keyword.lower() for keyword in skill_keywords):
@@ -141,7 +140,7 @@ class RouterGenerator:
return routing
def _extract_skill_specific_labels(self, skill_name: str, skill_keywords: set) -> List[str]:
def _extract_skill_specific_labels(self, skill_name: str, skill_keywords: set) -> list[str]:
"""
Extract labels from GitHub issues that match this specific skill.
@@ -159,14 +158,14 @@ class RouterGenerator:
if not self.github_issues:
return []
common_problems = self.github_issues.get('common_problems', [])
known_solutions = self.github_issues.get('known_solutions', [])
common_problems = self.github_issues.get("common_problems", [])
known_solutions = self.github_issues.get("known_solutions", [])
all_issues = common_problems + known_solutions
matching_labels = set()
for issue in all_issues:
issue_labels = issue.get('labels', [])
issue_labels = issue.get("labels", [])
issue_labels_lower = [label.lower() for label in issue_labels]
# Check if this issue relates to the skill
@@ -180,13 +179,20 @@ class RouterGenerator:
# Add ALL labels from this matching issue
for label in issue_labels_lower:
# Skip generic labels that don't add routing value
if label not in ['bug', 'enhancement', 'question', 'help wanted',
'good first issue', 'documentation', 'duplicate']:
if label not in [
"bug",
"enhancement",
"question",
"help wanted",
"good first issue",
"documentation",
"duplicate",
]:
matching_labels.add(label)
return list(matching_labels)
def _generate_frontmatter(self, routing_keywords: Dict[str, List[str]]) -> str:
def _generate_frontmatter(self, routing_keywords: dict[str, list[str]]) -> str:
"""
Generate YAML frontmatter compliant with agentskills.io spec.
@@ -201,16 +207,16 @@ class RouterGenerator:
# Build comprehensive description from all sub-skills
all_topics = []
for config in self.configs:
desc = config.get('description', '')
desc = config.get("description", "")
# Extract key topics from description (simple extraction)
topics = [word.strip() for word in desc.split(',') if word.strip()]
topics = [word.strip() for word in desc.split(",") if word.strip()]
all_topics.extend(topics[:2]) # Max 2 topics per skill
# Create keyword-rich description
unique_topics = list(dict.fromkeys(all_topics))[:7] # Top 7 unique topics
if unique_topics:
topics_str = ', '.join(unique_topics)
topics_str = ", ".join(unique_topics)
description = f"{self.router_name.title()} framework. Use when working with: {topics_str}"
else:
description = f"Use when working with {self.router_name.title()} development and programming"
@@ -225,21 +231,21 @@ class RouterGenerator:
# Try to get language-specific compatibility if GitHub metadata available
if self.github_metadata:
language = self.github_metadata.get('language', '')
language = self.github_metadata.get("language", "")
compatibility_map = {
'Python': f'Python 3.10+, requires {self.router_name} package',
'JavaScript': f'Node.js 18+, requires {self.router_name} package',
'TypeScript': f'Node.js 18+, TypeScript 5+, requires {self.router_name} package',
'Go': f'Go 1.20+, requires {self.router_name} package',
'Rust': f'Rust 1.70+, requires {self.router_name} package',
'Java': f'Java 17+, requires {self.router_name} package',
"Python": f"Python 3.10+, requires {self.router_name} package",
"JavaScript": f"Node.js 18+, requires {self.router_name} package",
"TypeScript": f"Node.js 18+, TypeScript 5+, requires {self.router_name} package",
"Go": f"Go 1.20+, requires {self.router_name} package",
"Rust": f"Rust 1.70+, requires {self.router_name} package",
"Java": f"Java 17+, requires {self.router_name} package",
}
if language in compatibility_map:
compatibility = compatibility_map[language]
# Try to extract license
if isinstance(self.github_metadata.get('license'), dict):
license_info = self.github_metadata['license'].get('name', 'MIT')
if isinstance(self.github_metadata.get("license"), dict):
license_info = self.github_metadata["license"].get("name", "MIT")
frontmatter = f"""---
name: {self.router_name}
@@ -289,27 +295,27 @@ compatibility: {compatibility}
"""
# Remove router name prefix
if skill_name.startswith(f"{self.router_name}-"):
topic = skill_name[len(self.router_name)+1:]
topic = skill_name[len(self.router_name) + 1 :]
else:
topic = skill_name
# Capitalize and add context
topic = topic.replace('-', ' ').title()
topic = topic.replace("-", " ").title()
# Add common suffixes for context
topic_map = {
'oauth': 'OAuth authentication',
'auth': 'authentication',
'async': 'async patterns',
'api': 'API integration',
'orm': 'ORM queries',
'hooks': 'hooks',
'routing': 'routing',
'testing': 'testing',
'2d': '2D development',
'3d': '3D development',
'scripting': 'scripting',
'physics': 'physics',
"oauth": "OAuth authentication",
"auth": "authentication",
"async": "async patterns",
"api": "API integration",
"orm": "ORM queries",
"hooks": "hooks",
"routing": "routing",
"testing": "testing",
"2d": "2D development",
"3d": "3D development",
"scripting": "scripting",
"physics": "physics",
}
topic_lower = topic.lower()
@@ -319,7 +325,7 @@ compatibility: {compatibility}
return topic
def _generate_dynamic_examples(self, routing_keywords: Dict[str, List[str]]) -> str:
def _generate_dynamic_examples(self, routing_keywords: dict[str, list[str]]) -> str:
"""
Generate examples dynamically from actual sub-skill names and keywords.
@@ -351,10 +357,7 @@ compatibility: {compatibility}
topic = self._extract_topic_from_skill(first_skill)
keyword = first_keywords[0] if first_keywords else topic
examples.append(
f'**Q:** "How do I implement {keyword}?"\n'
f'**A:** Activates {first_skill} skill'
)
examples.append(f'**Q:** "How do I implement {keyword}?"\n**A:** Activates {first_skill} skill')
# Example 2: Different skill (second sub-skill if available)
if len(skill_names) >= 2:
@@ -365,8 +368,7 @@ compatibility: {compatibility}
keyword = second_keywords[0] if second_keywords else topic
examples.append(
f'**Q:** "Working with {keyword} in {self.router_name.title()}"\n'
f'**A:** Activates {second_skill} skill'
f'**Q:** "Working with {keyword} in {self.router_name.title()}"\n**A:** Activates {second_skill} skill'
)
# Example 3: Multi-skill activation (if 2+ skills)
@@ -378,13 +380,12 @@ compatibility: {compatibility}
topic_2 = self._extract_topic_from_skill(skill_2)
examples.append(
f'**Q:** "Combining {topic_1} with {topic_2}"\n'
f'**A:** Activates {skill_1} + {skill_2} skills'
f'**Q:** "Combining {topic_1} with {topic_2}"\n**A:** Activates {skill_1} + {skill_2} skills'
)
return '\n\n'.join(examples)
return "\n\n".join(examples)
def _generate_examples_from_github(self, routing_keywords: Dict[str, List[str]]) -> str:
def _generate_examples_from_github(self, routing_keywords: dict[str, list[str]]) -> str:
"""
Generate examples from real GitHub issue titles.
@@ -402,7 +403,7 @@ compatibility: {compatibility}
return self._generate_dynamic_examples(routing_keywords)
examples = []
common_problems = self.github_issues.get('common_problems', [])
common_problems = self.github_issues.get("common_problems", [])
if not common_problems:
return self._generate_dynamic_examples(routing_keywords)
@@ -414,29 +415,26 @@ compatibility: {compatibility}
# Find first issue matching this skill's keywords
for issue in common_problems:
issue_labels = [label.lower() for label in issue.get('labels', [])]
issue_labels = [label.lower() for label in issue.get("labels", [])]
if any(label in skill_keywords_lower for label in issue_labels):
matched_issue = issue
common_problems.remove(issue) # Don't reuse same issue
break
if matched_issue:
title = matched_issue.get('title', '')
title = matched_issue.get("title", "")
question = self._convert_issue_to_question(title)
examples.append(
f'**Q:** "{question}"\n'
f'**A:** Activates {skill_name} skill'
)
examples.append(f'**Q:** "{question}"\n**A:** Activates {skill_name} skill')
else:
# Fallback to keyword-based example for this skill
topic = self._extract_topic_from_skill(skill_name)
keyword = keywords[0] if keywords else topic
examples.append(
f'**Q:** "Working with {keyword} in {self.router_name.title()}"\n'
f'**A:** Activates {skill_name} skill'
f"**A:** Activates {skill_name} skill"
)
return '\n\n'.join(examples) if examples else self._generate_dynamic_examples(routing_keywords)
return "\n\n".join(examples) if examples else self._generate_dynamic_examples(routing_keywords)
def _convert_issue_to_question(self, issue_title: str) -> str:
"""
@@ -456,24 +454,24 @@ compatibility: {compatibility}
title_lower = issue_title.lower()
# Pattern 1: Error/Failure issues
if 'fail' in title_lower or 'error' in title_lower or 'issue' in title_lower:
cleaned = issue_title.replace(' fails', '').replace(' errors', '').replace(' issue', '')
if "fail" in title_lower or "error" in title_lower or "issue" in title_lower:
cleaned = issue_title.replace(" fails", "").replace(" errors", "").replace(" issue", "")
return f"How do I fix {cleaned.lower()}?"
# Pattern 2: Documentation requests
if 'documentation' in title_lower or 'docs' in title_lower:
cleaned = issue_title.replace(' documentation', '').replace(' docs', '')
if "documentation" in title_lower or "docs" in title_lower:
cleaned = issue_title.replace(" documentation", "").replace(" docs", "")
return f"How do I use {cleaned.lower()}?"
# Pattern 3: Feature requests
if title_lower.startswith('add ') or title_lower.startswith('added '):
feature = issue_title.replace('Add ', '').replace('Added ', '')
if title_lower.startswith("add ") or title_lower.startswith("added "):
feature = issue_title.replace("Add ", "").replace("Added ", "")
return f"How do I implement {feature.lower()}?"
# Default: Generic question
return f"How do I handle {issue_title.lower()}?"
def _extract_common_patterns(self) -> List[Dict[str, str]]:
def _extract_common_patterns(self) -> list[dict[str, str]]:
"""
Extract problem-solution patterns from closed GitHub issues.
@@ -487,25 +485,21 @@ compatibility: {compatibility}
if not self.github_issues:
return []
known_solutions = self.github_issues.get('known_solutions', [])
known_solutions = self.github_issues.get("known_solutions", [])
if not known_solutions:
return []
patterns = []
# Top 5 closed issues with most engagement (comments indicate usefulness)
top_solutions = sorted(known_solutions, key=lambda x: x.get('comments', 0), reverse=True)[:5]
top_solutions = sorted(known_solutions, key=lambda x: x.get("comments", 0), reverse=True)[:5]
for issue in top_solutions:
title = issue.get('title', '')
number = issue.get('number', 0)
title = issue.get("title", "")
number = issue.get("number", 0)
problem, solution = self._parse_issue_pattern(title)
patterns.append({
'problem': problem,
'solution': solution,
'issue_number': number
})
patterns.append({"problem": problem, "solution": solution, "issue_number": number})
return patterns
@@ -530,24 +524,24 @@ compatibility: {compatibility}
title_lower = issue_title.lower()
# Pattern 1: "Fixed X" → "X not working" / "See fix"
if title_lower.startswith('fixed ') or title_lower.startswith('fix '):
problem_text = issue_title.replace('Fixed ', '').replace('Fix ', '')
if title_lower.startswith("fixed ") or title_lower.startswith("fix "):
problem_text = issue_title.replace("Fixed ", "").replace("Fix ", "")
return (f"{problem_text} not working", "See fix implementation details")
# Pattern 2: "Resolved X" → "X issue" / "See resolution"
if title_lower.startswith('resolved ') or title_lower.startswith('resolve '):
problem_text = issue_title.replace('Resolved ', '').replace('Resolve ', '')
if title_lower.startswith("resolved ") or title_lower.startswith("resolve "):
problem_text = issue_title.replace("Resolved ", "").replace("Resolve ", "")
return (f"{problem_text} issue", "See resolution approach")
# Pattern 3: "Added X" → "Missing X" / "Use X"
if title_lower.startswith('added ') or title_lower.startswith('add '):
feature_text = issue_title.replace('Added ', '').replace('Add ', '')
if title_lower.startswith("added ") or title_lower.startswith("add "):
feature_text = issue_title.replace("Added ", "").replace("Add ", "")
return (f"Missing {feature_text}", f"Use {feature_text} feature")
# Default: Use title as-is
return (issue_title, "See issue for solution details")
def _detect_framework(self) -> Optional[str]:
def _detect_framework(self) -> str | None:
"""
Detect framework from router name and GitHub metadata.
@@ -561,14 +555,14 @@ compatibility: {compatibility}
router_lower = self.router_name.lower()
framework_keywords = {
'fastapi': 'fastapi',
'django': 'django',
'flask': 'flask',
'react': 'react',
'vue': 'vue',
'express': 'express',
'fastmcp': 'fastmcp',
'mcp': 'fastmcp',
"fastapi": "fastapi",
"django": "django",
"flask": "flask",
"react": "react",
"vue": "vue",
"express": "express",
"fastmcp": "fastmcp",
"mcp": "fastmcp",
}
# Check router name first
@@ -578,7 +572,7 @@ compatibility: {compatibility}
# Check GitHub description if available
if self.github_metadata:
description = self.github_metadata.get('description', '').lower()
description = self.github_metadata.get("description", "").lower()
for keyword, framework in framework_keywords.items():
if keyword in description:
return framework
@@ -599,7 +593,7 @@ compatibility: {compatibility}
Formatted Quick Start section with install + hello world code
"""
templates = {
'fastapi': """## Quick Start
"fastapi": """## Quick Start
```bash
pip install fastapi uvicorn
@@ -617,7 +611,7 @@ def read_root():
# Run: uvicorn main:app --reload
```
""",
'fastmcp': """## Quick Start
"fastmcp": """## Quick Start
```bash
pip install fastmcp
@@ -633,7 +627,7 @@ def greet(name: str) -> str:
return f"Hello, {name}!"
```
""",
'django': """## Quick Start
"django": """## Quick Start
```bash
pip install django
@@ -644,7 +638,7 @@ python manage.py runserver
Visit http://127.0.0.1:8000/ to see your Django app.
""",
'react': """## Quick Start
"react": """## Quick Start
```bash
npx create-react-app my-app
@@ -677,16 +671,16 @@ export default App;
all_topics = []
for config in self.configs:
desc = config.get('description', '')
desc = config.get("description", "")
# Extract key topics from description (simple comma-separated extraction)
topics = [topic.strip() for topic in desc.split(',') if topic.strip()]
topics = [topic.strip() for topic in desc.split(",") if topic.strip()]
all_topics.extend(topics[:2]) # Max 2 topics per skill
# Deduplicate and take top 5-7 topics
unique_topics = list(dict.fromkeys(all_topics))[:7]
if not unique_topics:
return f'Use when working with {self.router_name} development and programming'
return f"Use when working with {self.router_name} development and programming"
# Format as user-friendly bulleted list
description = f"""Use this skill when working with:
@@ -695,8 +689,8 @@ export default App;
for topic in unique_topics:
# Clean up topic text (remove "when working with" prefixes if present)
topic = topic.replace('when working with', '').strip()
topic = topic.replace('Use when', '').strip()
topic = topic.replace("when working with", "").strip()
topic = topic.replace("Use when", "").strip()
if topic:
description += f"- {topic}\n"
@@ -721,7 +715,10 @@ export default App;
# NEW: Generate comprehensive description from all sub-skills
when_to_use = self._generate_comprehensive_description()
skill_md = frontmatter + "\n\n" + f"""# {self.router_name.replace('-', ' ').title()} Documentation
skill_md = (
frontmatter
+ "\n\n"
+ f"""# {self.router_name.replace("-", " ").title()} Documentation
## When to Use This Skill
@@ -730,26 +727,27 @@ export default App;
This is a router skill that directs your questions to specialized sub-skills for efficient, focused assistance.
"""
)
# Phase 4: Add GitHub repository metadata
if self.github_metadata:
# NEW: Use html_url from GitHub metadata instead of base_url from config
repo_url = self.github_metadata.get('html_url', '')
stars = self.github_metadata.get('stars', 0)
language = self.github_metadata.get('language', 'Unknown')
description = self.github_metadata.get('description', '')
repo_url = self.github_metadata.get("html_url", "")
stars = self.github_metadata.get("stars", 0)
language = self.github_metadata.get("language", "Unknown")
description = self.github_metadata.get("description", "")
skill_md += f"""## Repository Info
**Repository:** {repo_url}
**Stars:** ⭐ {stars:,} | **Language:** {language}
{f'**Description:** {description}' if description else ''}
{f"**Description:** {description}" if description else ""}
"""
# Phase 4: Add Quick Start from README
if self.github_docs and self.github_docs.get('readme'):
readme = self.github_docs['readme']
if self.github_docs and self.github_docs.get("readme"):
readme = self.github_docs["readme"]
# NEW: Clean HTML and extract meaningful content
quick_start = self._extract_clean_readme_section(readme)
@@ -768,14 +766,20 @@ This is a router skill that directs your questions to specialized sub-skills for
if framework:
hello_world = self._get_framework_hello_world(framework)
if hello_world:
skill_md += hello_world + "\n*Note: Generic template. See references/getting_started.md for project-specific setup.*\n\n"
skill_md += (
hello_world
+ "\n*Note: Generic template. See references/getting_started.md for project-specific setup.*\n\n"
)
else:
# No README available - try framework fallback
framework = self._detect_framework()
if framework:
hello_world = self._get_framework_hello_world(framework)
if hello_world:
skill_md += hello_world + "\n*Note: Generic template. Check repository for specific installation instructions.*\n\n"
skill_md += (
hello_world
+ "\n*Note: Generic template. Check repository for specific installation instructions.*\n\n"
)
skill_md += """## How It Works
@@ -785,11 +789,11 @@ This skill analyzes your question and activates the appropriate specialized skil
# List sub-skills
for config in self.configs:
name = config['name']
desc = config.get('description', '')
name = config["name"]
desc = config.get("description", "")
# Remove router name prefix from description if present
if desc.startswith(f"{self.router_name.title()} -"):
desc = desc.split(' - ', 1)[1]
desc = desc.split(" - ", 1)[1]
skill_md += f"### {name}\n{desc}\n\n"
@@ -808,7 +812,7 @@ The router analyzes your question for topic keywords and activates relevant skil
skill_md += f"- {keyword_str} → **{skill_name}**\n"
# Quick reference
skill_md += f"""
skill_md += """
## Quick Reference
@@ -839,7 +843,7 @@ For quick answers, this router provides basic overview information. For detailed
# Phase 4: Add Common Issues from GitHub (Summary with Reference)
if self.github_issues:
common_problems = self.github_issues.get('common_problems', [])[:5] # Top 5
common_problems = self.github_issues.get("common_problems", [])[:5] # Top 5
if common_problems:
skill_md += """
@@ -850,9 +854,9 @@ Top 5 GitHub issues from the community:
"""
for i, issue in enumerate(common_problems, 1):
title = issue.get('title', '')
number = issue.get('number', 0)
comments = issue.get('comments', 0)
title = issue.get("title", "")
number = issue.get("number", 0)
comments = issue.get("comments", 0)
skill_md += f"{i}. **{title}** (Issue #{number}, {comments} comments)\n"
@@ -871,9 +875,9 @@ Problem-solution patterns from resolved GitHub issues:
"""
for i, pattern in enumerate(patterns, 1):
problem = pattern['problem']
solution = pattern['solution']
issue_num = pattern['issue_number']
problem = pattern["problem"]
solution = pattern["solution"]
issue_num = pattern["issue_number"]
skill_md += f"**Pattern {i}**: {problem}\n"
skill_md += f"→ **Solution**: {solution} ([Issue #{issue_num}](references/github_issues.md))\n\n"
@@ -888,10 +892,10 @@ Detailed documentation available in:
"""
if self.github_issues:
skill_md += "- `references/github_issues.md` - Community problems and solutions\n"
if self.github_docs and self.github_docs.get('readme'):
if self.github_docs and self.github_docs.get("readme"):
skill_md += "- `references/getting_started.md` - Detailed setup guide\n"
skill_md += f"""
skill_md += """
## Need Help?
@@ -904,7 +908,7 @@ Simply ask your question and mention the topic. The router will find the right s
return skill_md
def generate_subskill_issues_section(self, skill_name: str, topics: List[str]) -> str:
def generate_subskill_issues_section(self, skill_name: str, topics: list[str]) -> str:
"""
Generate "Common Issues" section for a sub-skill (Phase 4).
@@ -918,8 +922,8 @@ Simply ask your question and mention the topic. The router will find the right s
if not self.github_issues or not categorize_issues_by_topic:
return ""
common_problems = self.github_issues.get('common_problems', [])
known_solutions = self.github_issues.get('known_solutions', [])
common_problems = self.github_issues.get("common_problems", [])
known_solutions = self.github_issues.get("known_solutions", [])
# Categorize issues by topic
categorized = categorize_issues_by_topic(common_problems, known_solutions, topics)
@@ -944,11 +948,11 @@ GitHub issues related to this topic:
issues_md += f"\n### {topic.title()}\n\n"
for issue in issues[:3]: # Top 3 per topic
title = issue.get('title', '')
number = issue.get('number', 0)
state = issue.get('state', 'unknown')
comments = issue.get('comments', 0)
labels = issue.get('labels', [])
title = issue.get("title", "")
number = issue.get("number", 0)
state = issue.get("state", "unknown")
comments = issue.get("comments", 0)
labels = issue.get("labels", [])
# Format issue
state_icon = "🔴" if state == "open" else ""
@@ -964,21 +968,24 @@ GitHub issues related to this topic:
return issues_md
def create_router_config(self) -> Dict[str, Any]:
def create_router_config(self) -> dict[str, Any]:
"""Create router configuration"""
routing_keywords = self.extract_routing_keywords()
router_config = {
"name": self.router_name,
"description": self.base_config.get('description', f'Use when working with {self.router_name} documentation (router for multiple sub-skills)'),
"base_url": self.base_config['base_url'],
"selectors": self.base_config.get('selectors', {}),
"url_patterns": self.base_config.get('url_patterns', {}),
"rate_limit": self.base_config.get('rate_limit', 0.5),
"description": self.base_config.get(
"description",
f"Use when working with {self.router_name} documentation (router for multiple sub-skills)",
),
"base_url": self.base_config["base_url"],
"selectors": self.base_config.get("selectors", {}),
"url_patterns": self.base_config.get("url_patterns", {}),
"rate_limit": self.base_config.get("rate_limit", 0.5),
"max_pages": 500, # Router only scrapes overview pages
"_router": True,
"_sub_skills": [cfg['name'] for cfg in self.configs],
"_routing_keywords": routing_keywords
"_sub_skills": [cfg["name"] for cfg in self.configs],
"_routing_keywords": routing_keywords,
}
return router_config
@@ -993,34 +1000,38 @@ GitHub issues related to this topic:
md = "# Common GitHub Issues\n\n"
md += "Top issues reported by the community:\n\n"
common_problems = self.github_issues.get('common_problems', [])[:10] if self.github_issues else []
known_solutions = self.github_issues.get('known_solutions', [])[:10] if self.github_issues else []
common_problems = self.github_issues.get("common_problems", [])[:10] if self.github_issues else []
known_solutions = self.github_issues.get("known_solutions", [])[:10] if self.github_issues else []
if common_problems:
md += "## Open Issues (Common Problems)\n\n"
for i, issue in enumerate(common_problems, 1):
title = issue.get('title', '')
number = issue.get('number', 0)
comments = issue.get('comments', 0)
labels = issue.get('labels', [])
title = issue.get("title", "")
number = issue.get("number", 0)
comments = issue.get("comments", 0)
labels = issue.get("labels", [])
if isinstance(labels, list):
labels_str = ', '.join(str(label) for label in labels)
labels_str = ", ".join(str(label) for label in labels)
else:
labels_str = str(labels) if labels else ''
labels_str = str(labels) if labels else ""
md += f"### {i}. {title}\n\n"
md += f"**Issue**: #{number}\n"
md += f"**Comments**: {comments}\n"
if labels_str:
md += f"**Labels**: {labels_str}\n"
md += f"**Link**: https://github.com/{self.github_metadata.get('html_url', '').replace('https://github.com/', '')}/issues/{number}\n\n" if self.github_metadata else "\n\n"
md += (
f"**Link**: https://github.com/{self.github_metadata.get('html_url', '').replace('https://github.com/', '')}/issues/{number}\n\n"
if self.github_metadata
else "\n\n"
)
if known_solutions:
md += "\n## Closed Issues (Known Solutions)\n\n"
for i, issue in enumerate(known_solutions, 1):
title = issue.get('title', '')
number = issue.get('number', 0)
comments = issue.get('comments', 0)
title = issue.get("title", "")
number = issue.get("number", 0)
comments = issue.get("comments", 0)
md += f"### {i}. {title}\n\n"
md += f"**Issue**: #{number} (Closed)\n"
@@ -1042,8 +1053,8 @@ GitHub issues related to this topic:
md = "# Getting Started\n\n"
md += "*Extracted from project README*\n\n"
if self.github_docs and self.github_docs.get('readme'):
readme = self.github_docs['readme']
if self.github_docs and self.github_docs.get("readme"):
readme = self.github_docs["readme"]
# Clean and extract full quick start section (up to 2000 chars)
cleaner = MarkdownCleaner()
@@ -1069,16 +1080,16 @@ GitHub issues related to this topic:
# 1. GitHub Issues Reference
if self.github_issues:
issues_md = self._generate_github_issues_reference()
with open(references_dir / 'github_issues.md', 'w') as f:
with open(references_dir / "github_issues.md", "w") as f:
f.write(issues_md)
# 2. Getting Started Reference
if self.github_docs and self.github_docs.get('readme'):
if self.github_docs and self.github_docs.get("readme"):
getting_started_md = self._generate_getting_started_reference()
with open(references_dir / 'getting_started.md', 'w') as f:
with open(references_dir / "getting_started.md", "w") as f:
f.write(getting_started_md)
def generate(self, output_dir: Path = None) -> Tuple[Path, Path]:
def generate(self, output_dir: Path = None) -> tuple[Path, Path]:
"""Generate router skill and config with progressive disclosure"""
if output_dir is None:
output_dir = self.config_paths[0].parent
@@ -1090,11 +1101,11 @@ GitHub issues related to this topic:
skill_path = output_dir.parent / f"output/{self.router_name}/SKILL.md"
skill_path.parent.mkdir(parents=True, exist_ok=True)
with open(skill_path, 'w') as f:
with open(skill_path, "w") as f:
f.write(skill_md)
# NEW: Create references/ directory and generate reference files
references_dir = skill_path.parent / 'references'
references_dir = skill_path.parent / "references"
references_dir.mkdir(parents=True, exist_ok=True)
self._generate_reference_files(references_dir)
@@ -1102,7 +1113,7 @@ GitHub issues related to this topic:
router_config = self.create_router_config()
config_path = output_dir / f"{self.router_name}.json"
with open(config_path, 'w') as f:
with open(config_path, "w") as f:
json.dump(router_config, f, indent=2)
return config_path, skill_path
@@ -1125,24 +1136,14 @@ Examples:
# Custom output directory
python3 generate_router.py configs/godot-*.json --output-dir configs/routers/
"""
""",
)
parser.add_argument(
'configs',
nargs='+',
help='Sub-skill config files'
)
parser.add_argument("configs", nargs="+", help="Sub-skill config files")
parser.add_argument(
'--name',
help='Router skill name (default: inferred from sub-skills)'
)
parser.add_argument("--name", help="Router skill name (default: inferred from sub-skills)")
parser.add_argument(
'--output-dir',
help='Output directory (default: same as input configs)'
)
parser.add_argument("--output-dir", help="Output directory (default: same as input configs)")
args = parser.parse_args()
@@ -1150,16 +1151,16 @@ Examples:
config_files = []
for path_str in args.configs:
path = Path(path_str)
if path.exists() and not path.stem.endswith('-router'):
if path.exists() and not path.stem.endswith("-router"):
config_files.append(path_str)
if not config_files:
print("❌ Error: No valid config files provided")
sys.exit(1)
print(f"\n{'='*60}")
print(f"\n{'=' * 60}")
print("ROUTER SKILL GENERATOR")
print(f"{'='*60}")
print(f"{'=' * 60}")
print(f"Sub-skills: {len(config_files)}")
for cfg in config_files:
print(f" - {Path(cfg).stem}")
@@ -1172,11 +1173,11 @@ Examples:
print(f"✅ Router config created: {config_path}")
print(f"✅ Router SKILL.md created: {skill_path}")
print("")
print(f"{'='*60}")
print(f"{'=' * 60}")
print("NEXT STEPS")
print(f"{'='*60}")
print(f"{'=' * 60}")
print(f"1. Review router SKILL.md: {skill_path}")
print(f"2. Optionally scrape router (for overview pages):")
print("2. Optionally scrape router (for overview pages):")
print(f" skill-seekers scrape --config {config_path}")
print("3. Package router skill:")
print(f" skill-seekers package output/{generator.router_name}/")