Second batch of comprehensive linting fixes: Unused Arguments/Variables (136 errors): - ARG002/ARG001 (91 errors): Prefixed unused method/function arguments with '_' - Interface methods in adaptors (base.py, gemini.py, markdown.py) - AST analyzer methods maintaining signatures (code_analyzer.py) - Test fixtures and hooks (conftest.py) - Added noqa: ARG001/ARG002 for pytest hooks requiring exact names - F841 (45 errors): Prefixed unused local variables with '_' - Tuple unpacking where some values aren't needed - Variables assigned but not referenced Loop & Boolean Quality (28 errors): - B007 (18 errors): Prefixed unused loop control variables with '_' - enumerate() loops where index not used - for-in loops where loop variable not referenced - E712 (10 errors): Simplified boolean comparisons - Changed '== True' to direct boolean check - Changed '== False' to 'not' expression - Improved test readability Code Quality (24 errors): - SIM201 (4 errors): Already fixed in previous commit - SIM118 (2 errors): Already fixed in previous commit - E741 (4 errors): Already fixed in previous commit - Config manager loop variable fix (1 error) All Tests Passing: - test_scraper_features.py: 42 passed - test_integration.py: 51 passed - test_architecture_scenarios.py: 11 passed - test_real_world_fastmcp.py: 19 passed, 1 skipped Note: Some SIM errors (nested if, multiple with) remain unfixed as they would require non-trivial refactoring. Focus was on functional correctness. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1250 lines
43 KiB
Python
1250 lines
43 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
How-To Guide Builder (C3.3) - Build step-by-step guides from workflow examples
|
|
|
|
Transforms multi-step test workflows (from C3.2) into educational "how to" guides with:
|
|
- Step-by-step breakdowns
|
|
- Prerequisites and setup requirements
|
|
- Verification checkpoints
|
|
- Troubleshooting sections
|
|
- Complexity levels (beginner/intermediate/advanced)
|
|
|
|
Usage:
|
|
# From test examples JSON
|
|
skill-seekers build-how-to-guides --input test_examples.json
|
|
|
|
# From directory (auto-extracts workflows)
|
|
skill-seekers build-how-to-guides tests/
|
|
|
|
# With AI enhancement
|
|
skill-seekers build-how-to-guides tests/ --enhance-with-ai
|
|
|
|
Example workflow → guide transformation:
|
|
Input: Multi-step test showing user registration + login + session
|
|
Output: "How To: Complete User Authentication" guide with:
|
|
- 5 discrete steps with explanations
|
|
- Prerequisites (database, email service)
|
|
- Verification points at each step
|
|
- Common pitfalls and troubleshooting
|
|
- Related guides suggestions
|
|
"""
|
|
|
|
import ast
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import re
|
|
from collections import defaultdict
|
|
from dataclasses import asdict, dataclass, field
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Literal
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# DATA MODELS
|
|
# ============================================================================
|
|
|
|
|
|
@dataclass
|
|
class PrerequisiteItem:
|
|
"""Enhanced prerequisite with explanation (AI enhancement)"""
|
|
|
|
name: str
|
|
why: str # Why this is needed
|
|
setup: str # How to install/configure
|
|
|
|
|
|
@dataclass
|
|
class TroubleshootingItem:
|
|
"""Enhanced troubleshooting with solutions (AI enhancement)"""
|
|
|
|
problem: str
|
|
symptoms: list[str] = field(default_factory=list) # How to recognize this issue
|
|
solution: str = "" # Step-by-step fix
|
|
diagnostic_steps: list[str] = field(default_factory=list) # How to diagnose
|
|
|
|
|
|
@dataclass
|
|
class WorkflowStep:
|
|
"""Single step in a workflow guide"""
|
|
|
|
step_number: int
|
|
code: str
|
|
description: str
|
|
expected_result: str | None = None
|
|
verification: str | None = None # Assertion or checkpoint
|
|
setup_required: str | None = None
|
|
explanation: str | None = None # Why this step matters
|
|
common_pitfall: str | None = None # Warning for this step
|
|
common_variations: list[str] = field(default_factory=list) # AI: Alternative approaches
|
|
|
|
|
|
@dataclass
|
|
class HowToGuide:
|
|
"""Complete how-to guide generated from workflow(s)"""
|
|
|
|
guide_id: str
|
|
title: str
|
|
overview: str
|
|
complexity_level: Literal["beginner", "intermediate", "advanced"]
|
|
|
|
# Prerequisites
|
|
prerequisites: list[str] = field(default_factory=list)
|
|
required_imports: list[str] = field(default_factory=list)
|
|
required_fixtures: list[str] = field(default_factory=list)
|
|
|
|
# Content
|
|
workflows: list[dict] = field(default_factory=list) # Source workflow examples
|
|
steps: list[WorkflowStep] = field(default_factory=list)
|
|
|
|
# Metadata
|
|
use_case: str = ""
|
|
tags: list[str] = field(default_factory=list)
|
|
estimated_time: str = "10 minutes"
|
|
source_files: list[str] = field(default_factory=list)
|
|
|
|
# Optional AI enhancement (basic)
|
|
common_pitfalls: list[str] = field(default_factory=list)
|
|
troubleshooting: dict[str, str] = field(default_factory=dict)
|
|
variations: list[str] = field(default_factory=list)
|
|
related_guides: list[str] = field(default_factory=list)
|
|
|
|
# AI enhancement (comprehensive - NEW)
|
|
prerequisites_detailed: list[PrerequisiteItem] = field(default_factory=list)
|
|
troubleshooting_detailed: list[TroubleshootingItem] = field(default_factory=list)
|
|
next_steps_detailed: list[str] = field(default_factory=list)
|
|
use_cases: list[str] = field(default_factory=list)
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary"""
|
|
result = asdict(self)
|
|
# Convert WorkflowStep objects to dicts
|
|
result["steps"] = [asdict(step) for step in self.steps]
|
|
return result
|
|
|
|
|
|
@dataclass
|
|
class GuideCollection:
|
|
"""Collection of guides organized by category"""
|
|
|
|
total_guides: int
|
|
guides_by_complexity: dict[str, int]
|
|
guides_by_use_case: dict[str, list[HowToGuide]]
|
|
guides: list[HowToGuide]
|
|
|
|
def to_dict(self) -> dict:
|
|
"""Convert to dictionary"""
|
|
return {
|
|
"total_guides": self.total_guides,
|
|
"guides_by_complexity": self.guides_by_complexity,
|
|
"guides_by_use_case": {
|
|
k: [g.to_dict() for g in v] for k, v in self.guides_by_use_case.items()
|
|
},
|
|
"guides": [g.to_dict() for g in self.guides],
|
|
}
|
|
|
|
|
|
# ============================================================================
|
|
# WORKFLOW ANALYZER
|
|
# ============================================================================
|
|
|
|
|
|
class WorkflowAnalyzer:
|
|
"""Analyze workflow examples to extract steps and metadata"""
|
|
|
|
def analyze_workflow(self, workflow: dict) -> tuple[list[WorkflowStep], dict]:
|
|
"""
|
|
Deep analysis of workflow structure.
|
|
|
|
Args:
|
|
workflow: TestExample dict from C3.2
|
|
|
|
Returns:
|
|
(steps, metadata) where metadata includes prerequisites, complexity, etc.
|
|
"""
|
|
code = workflow.get("code", "")
|
|
language = workflow.get("language", "python").lower()
|
|
|
|
# Extract steps based on language
|
|
if language == "python":
|
|
steps = self._extract_steps_python(code, workflow)
|
|
else:
|
|
steps = self._extract_steps_heuristic(code, workflow)
|
|
|
|
# Detect prerequisites
|
|
metadata = self._detect_prerequisites(workflow)
|
|
|
|
# Find verification points
|
|
verifications = self._find_verification_points(code)
|
|
|
|
# Associate verifications with steps
|
|
for i, step in enumerate(steps):
|
|
if i < len(verifications):
|
|
step.verification = verifications[i]
|
|
|
|
# Calculate complexity
|
|
metadata["complexity_level"] = self._calculate_complexity(steps, workflow)
|
|
metadata["estimated_time"] = self._estimate_time(steps)
|
|
|
|
return steps, metadata
|
|
|
|
def _extract_steps_python(self, code: str, workflow: dict) -> list[WorkflowStep]:
|
|
"""Extract steps from Python code using AST"""
|
|
steps = []
|
|
|
|
try:
|
|
tree = ast.parse(code)
|
|
statements = []
|
|
|
|
# Collect all statements
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, (ast.Assign, ast.Expr, ast.Assert)):
|
|
statements.append(node)
|
|
|
|
step_num = 1
|
|
for stmt in statements:
|
|
# Skip assertions for now (they're verifications)
|
|
if isinstance(stmt, ast.Assert):
|
|
continue
|
|
|
|
# Get code for this statement
|
|
step_code = ast.get_source_segment(code, stmt)
|
|
if not step_code:
|
|
continue
|
|
|
|
# Generate description from code
|
|
description = self._generate_step_description(stmt, step_code)
|
|
|
|
# Check if next statement is assertion (verification)
|
|
idx = statements.index(stmt)
|
|
verification = None
|
|
if idx + 1 < len(statements) and isinstance(statements[idx + 1], ast.Assert):
|
|
verification = ast.get_source_segment(code, statements[idx + 1])
|
|
|
|
steps.append(
|
|
WorkflowStep(
|
|
step_number=step_num,
|
|
code=step_code,
|
|
description=description,
|
|
verification=verification,
|
|
)
|
|
)
|
|
step_num += 1
|
|
|
|
except SyntaxError:
|
|
# Fall back to heuristic method
|
|
return self._extract_steps_heuristic(code, workflow)
|
|
|
|
return steps
|
|
|
|
def _extract_steps_heuristic(self, code: str, _workflow: dict) -> list[WorkflowStep]:
|
|
"""Extract steps using heuristics (for non-Python or invalid syntax)"""
|
|
steps = []
|
|
lines = code.split("\n")
|
|
|
|
current_step = []
|
|
step_num = 1
|
|
|
|
for line in lines:
|
|
line_stripped = line.strip()
|
|
|
|
# Skip empty lines and comments
|
|
if not line_stripped or line_stripped.startswith("#"):
|
|
if current_step:
|
|
# End of current step
|
|
step_code = "\n".join(current_step)
|
|
description = self._infer_description_from_code(step_code)
|
|
|
|
steps.append(
|
|
WorkflowStep(step_number=step_num, code=step_code, description=description)
|
|
)
|
|
step_num += 1
|
|
current_step = []
|
|
continue
|
|
|
|
current_step.append(line)
|
|
|
|
# Add final step
|
|
if current_step:
|
|
step_code = "\n".join(current_step)
|
|
description = self._infer_description_from_code(step_code)
|
|
steps.append(
|
|
WorkflowStep(step_number=step_num, code=step_code, description=description)
|
|
)
|
|
|
|
return steps
|
|
|
|
def _generate_step_description(self, node: ast.AST, code: str) -> str:
|
|
"""Generate human-readable description from AST node"""
|
|
if isinstance(node, ast.Assign):
|
|
targets = [self._get_name(t) for t in node.targets]
|
|
value_desc = self._describe_value(node.value)
|
|
return f"Assign {', '.join(targets)} = {value_desc}"
|
|
|
|
elif isinstance(node, ast.Expr):
|
|
if isinstance(node.value, ast.Call):
|
|
func_name = self._get_name(node.value.func)
|
|
return f"Call {func_name}()"
|
|
|
|
return code.split("\n")[0] # First line as fallback
|
|
|
|
def _describe_value(self, node: ast.AST) -> str:
|
|
"""Describe AST value node"""
|
|
if isinstance(node, ast.Call):
|
|
func_name = self._get_name(node.func)
|
|
return f"{func_name}(...)"
|
|
elif isinstance(node, ast.Constant):
|
|
return repr(node.value)
|
|
elif isinstance(node, ast.Name):
|
|
return node.id
|
|
return "value"
|
|
|
|
def _get_name(self, node: ast.AST) -> str:
|
|
"""Extract name from AST node"""
|
|
if isinstance(node, ast.Name):
|
|
return node.id
|
|
elif isinstance(node, ast.Attribute):
|
|
return f"{self._get_name(node.value)}.{node.attr}"
|
|
elif isinstance(node, ast.Call):
|
|
return self._get_name(node.func)
|
|
return "unknown"
|
|
|
|
def _infer_description_from_code(self, code: str) -> str:
|
|
"""Infer description from code using patterns"""
|
|
code = code.strip()
|
|
|
|
# Method call patterns
|
|
if "(" in code and ")" in code:
|
|
match = re.search(r"(\w+)\s*\(", code)
|
|
if match:
|
|
return f"Call {match.group(1)}()"
|
|
|
|
# Assignment patterns
|
|
if "=" in code and not code.startswith("assert"):
|
|
parts = code.split("=", 1)
|
|
var_name = parts[0].strip()
|
|
return f"Create {var_name}"
|
|
|
|
# Assertion patterns
|
|
if code.startswith("assert"):
|
|
return "Verify result"
|
|
|
|
return code.split("\n")[0] # First line
|
|
|
|
def _detect_prerequisites(self, workflow: dict) -> dict:
|
|
"""Detect prerequisites from workflow"""
|
|
metadata = {"prerequisites": [], "required_imports": [], "required_fixtures": []}
|
|
|
|
# Get dependencies from workflow
|
|
dependencies = workflow.get("dependencies", [])
|
|
metadata["required_imports"] = dependencies
|
|
|
|
# Get setup code
|
|
setup_code = workflow.get("setup_code")
|
|
if setup_code:
|
|
metadata["prerequisites"].append("Setup code must be executed first")
|
|
|
|
# Check for common fixtures in test name or setup
|
|
test_name = workflow.get("test_name", "").lower()
|
|
if "database" in test_name or (setup_code and "database" in setup_code.lower()):
|
|
metadata["required_fixtures"].append("database")
|
|
if "api" in test_name or (setup_code and "api" in setup_code.lower()):
|
|
metadata["required_fixtures"].append("api_client")
|
|
|
|
return metadata
|
|
|
|
def _find_verification_points(self, code: str) -> list[str]:
|
|
"""Find assertion statements in code"""
|
|
verifications = []
|
|
|
|
for line in code.split("\n"):
|
|
line_stripped = line.strip()
|
|
if line_stripped.startswith("assert"):
|
|
verifications.append(line_stripped)
|
|
|
|
return verifications
|
|
|
|
def _calculate_complexity(self, steps: list[WorkflowStep], workflow: dict) -> str:
|
|
"""Calculate complexity level"""
|
|
num_steps = len(steps)
|
|
|
|
# Check for advanced patterns
|
|
code = workflow.get("code", "")
|
|
has_async = "async" in code or "await" in code
|
|
has_mock = "mock" in code.lower() or "patch" in code.lower()
|
|
has_error_handling = "try" in code or "except" in code
|
|
|
|
_complexity_score = workflow.get("complexity_score", 0.5)
|
|
|
|
# Determine level
|
|
if num_steps <= 3 and not has_async and not has_mock:
|
|
return "beginner"
|
|
elif num_steps >= 8 or has_async or has_error_handling:
|
|
return "advanced"
|
|
else:
|
|
return "intermediate"
|
|
|
|
def _estimate_time(self, steps: list[WorkflowStep]) -> str:
|
|
"""Estimate time to complete guide"""
|
|
num_steps = len(steps)
|
|
|
|
if num_steps <= 3:
|
|
return "5 minutes"
|
|
elif num_steps <= 6:
|
|
return "10 minutes"
|
|
elif num_steps <= 10:
|
|
return "15 minutes"
|
|
else:
|
|
return "20 minutes"
|
|
|
|
|
|
# ============================================================================
|
|
# WORKFLOW GROUPER
|
|
# ============================================================================
|
|
|
|
|
|
class WorkflowGrouper:
|
|
"""Group related workflows into coherent guides"""
|
|
|
|
def group_workflows(
|
|
self, workflows: list[dict], strategy: str = "ai-tutorial-group"
|
|
) -> dict[str, list[dict]]:
|
|
"""
|
|
Group workflows using specified strategy.
|
|
|
|
Args:
|
|
workflows: List of workflow examples
|
|
strategy: "ai-tutorial-group", "file-path", "test-name", "complexity"
|
|
|
|
Returns:
|
|
Dict mapping group name to list of workflows
|
|
"""
|
|
if strategy == "ai-tutorial-group":
|
|
return self._group_by_ai_tutorial_group(workflows)
|
|
elif strategy == "file-path":
|
|
return self._group_by_file_path(workflows)
|
|
elif strategy == "test-name":
|
|
return self._group_by_test_name(workflows)
|
|
elif strategy == "complexity":
|
|
return self._group_by_complexity(workflows)
|
|
else:
|
|
# Default: AI tutorial group with fallback
|
|
groups = self._group_by_ai_tutorial_group(workflows)
|
|
if not groups or len(groups) == len(workflows):
|
|
# Fallback to file path if AI grouping didn't work well
|
|
groups = self._group_by_file_path(workflows)
|
|
return groups
|
|
|
|
def _group_by_ai_tutorial_group(self, workflows: list[dict]) -> dict[str, list[dict]]:
|
|
"""Group by AI-generated tutorial_group (from C3.6 enhancement)"""
|
|
groups = defaultdict(list)
|
|
ungrouped = []
|
|
|
|
for workflow in workflows:
|
|
ai_analysis = workflow.get("ai_analysis", {})
|
|
tutorial_group = ai_analysis.get("tutorial_group")
|
|
|
|
if tutorial_group:
|
|
groups[tutorial_group].append(workflow)
|
|
else:
|
|
ungrouped.append(workflow)
|
|
|
|
# Put ungrouped workflows in individual guides
|
|
for workflow in ungrouped:
|
|
test_name = workflow.get("test_name", "Unknown")
|
|
# Clean test name for title
|
|
title = self._clean_test_name(test_name)
|
|
groups[title] = [workflow]
|
|
|
|
return dict(groups)
|
|
|
|
def _group_by_file_path(self, workflows: list[dict]) -> dict[str, list[dict]]:
|
|
"""Group workflows from same test file"""
|
|
groups = defaultdict(list)
|
|
|
|
for workflow in workflows:
|
|
file_path = workflow.get("file_path", "")
|
|
# Extract meaningful name from file path
|
|
file_name = Path(file_path).stem if file_path else "Unknown"
|
|
# Remove test_ prefix
|
|
group_name = file_name.replace("test_", "").replace("_", " ").title()
|
|
groups[group_name].append(workflow)
|
|
|
|
return dict(groups)
|
|
|
|
def _group_by_test_name(self, workflows: list[dict]) -> dict[str, list[dict]]:
|
|
"""Group by common test name prefixes"""
|
|
groups = defaultdict(list)
|
|
|
|
for workflow in workflows:
|
|
test_name = workflow.get("test_name", "")
|
|
# Extract prefix (e.g., test_auth_login → auth)
|
|
prefix = self._extract_prefix(test_name)
|
|
groups[prefix].append(workflow)
|
|
|
|
return dict(groups)
|
|
|
|
def _group_by_complexity(self, workflows: list[dict]) -> dict[str, list[dict]]:
|
|
"""Group by complexity level"""
|
|
groups = {"Beginner": [], "Intermediate": [], "Advanced": []}
|
|
|
|
for workflow in workflows:
|
|
complexity_score = workflow.get("complexity_score", 0.5)
|
|
|
|
if complexity_score < 0.4:
|
|
groups["Beginner"].append(workflow)
|
|
elif complexity_score < 0.7:
|
|
groups["Intermediate"].append(workflow)
|
|
else:
|
|
groups["Advanced"].append(workflow)
|
|
|
|
# Remove empty groups
|
|
return {k: v for k, v in groups.items() if v}
|
|
|
|
def _clean_test_name(self, test_name: str) -> str:
|
|
"""Clean test name to readable title"""
|
|
# Remove test_ prefix
|
|
name = test_name.replace("test_", "")
|
|
# Replace underscores with spaces
|
|
name = name.replace("_", " ")
|
|
# Title case
|
|
return name.title()
|
|
|
|
def _extract_prefix(self, test_name: str) -> str:
|
|
"""Extract prefix from test name"""
|
|
# Remove test_ prefix
|
|
name = test_name.replace("test_", "")
|
|
# Get first part before underscore
|
|
parts = name.split("_")
|
|
if len(parts) > 1:
|
|
return parts[0].title()
|
|
return self._clean_test_name(test_name)
|
|
|
|
|
|
# ============================================================================
|
|
# GUIDE GENERATOR
|
|
# ============================================================================
|
|
|
|
|
|
class GuideGenerator:
|
|
"""Generate markdown guides from workflow data"""
|
|
|
|
def generate_guide_markdown(self, guide: HowToGuide) -> str:
|
|
"""
|
|
Generate complete markdown guide.
|
|
|
|
Args:
|
|
guide: HowToGuide object with all data
|
|
|
|
Returns:
|
|
Complete markdown string
|
|
"""
|
|
sections = []
|
|
|
|
# Header
|
|
sections.append(self._create_header(guide))
|
|
|
|
# Overview
|
|
sections.append(self._create_overview(guide))
|
|
|
|
# Prerequisites
|
|
if guide.prerequisites or guide.required_imports or guide.required_fixtures:
|
|
sections.append(self._create_prerequisites(guide))
|
|
|
|
# Step-by-step guide
|
|
sections.append(self._create_steps_section(guide.steps))
|
|
|
|
# Complete example
|
|
sections.append(self._create_complete_example(guide))
|
|
|
|
# Troubleshooting (if available)
|
|
if guide.common_pitfalls or guide.troubleshooting:
|
|
sections.append(self._create_troubleshooting(guide))
|
|
|
|
# Next steps and related guides
|
|
sections.append(self._create_next_steps(guide))
|
|
|
|
# Footer
|
|
sections.append(self._create_footer(guide))
|
|
|
|
return "\n\n".join(sections)
|
|
|
|
def _create_header(self, guide: HowToGuide) -> str:
|
|
"""Create guide header with metadata"""
|
|
lines = [f"# How To: {guide.title}"]
|
|
lines.append("")
|
|
lines.append(f"**Difficulty**: {guide.complexity_level.title()}")
|
|
lines.append(f"**Estimated Time**: {guide.estimated_time}")
|
|
|
|
if guide.tags:
|
|
lines.append(f"**Tags**: {', '.join(guide.tags)}")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def _create_overview(self, guide: HowToGuide) -> str:
|
|
"""Create overview section"""
|
|
return f"## Overview\n\n{guide.overview}"
|
|
|
|
def _create_prerequisites(self, guide: HowToGuide) -> str:
|
|
"""Create prerequisites section"""
|
|
lines = ["## Prerequisites"]
|
|
lines.append("")
|
|
|
|
# Checklist format
|
|
if guide.prerequisites:
|
|
for prereq in guide.prerequisites:
|
|
lines.append(f"- [ ] {prereq}")
|
|
lines.append("")
|
|
|
|
# Required imports
|
|
if guide.required_imports:
|
|
lines.append("**Required Modules:**")
|
|
for imp in guide.required_imports:
|
|
lines.append(f"- `{imp}`")
|
|
lines.append("")
|
|
|
|
# Required fixtures
|
|
if guide.required_fixtures:
|
|
lines.append("**Required Fixtures:**")
|
|
for fixture in guide.required_fixtures:
|
|
lines.append(f"- `{fixture}` fixture")
|
|
lines.append("")
|
|
|
|
# Setup code if available
|
|
if guide.workflows and guide.workflows[0].get("setup_code"):
|
|
setup_code = guide.workflows[0]["setup_code"]
|
|
lines.append("**Setup Required:**")
|
|
lines.append("```python")
|
|
lines.append(setup_code)
|
|
lines.append("```")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def _create_steps_section(self, steps: list[WorkflowStep]) -> str:
|
|
"""Create step-by-step guide section"""
|
|
lines = ["## Step-by-Step Guide"]
|
|
lines.append("")
|
|
|
|
for step in steps:
|
|
lines.append(f"### Step {step.step_number}: {step.description}")
|
|
lines.append("")
|
|
|
|
# Explanation if available
|
|
if step.explanation:
|
|
lines.append(f"**What you're doing:** {step.explanation}")
|
|
lines.append("")
|
|
|
|
# Code
|
|
lines.append("```python")
|
|
lines.append(step.code)
|
|
lines.append("```")
|
|
lines.append("")
|
|
|
|
# Expected result
|
|
if step.expected_result:
|
|
lines.append(f"**Expected Result:** {step.expected_result}")
|
|
lines.append("")
|
|
|
|
# Verification checkpoint
|
|
if step.verification:
|
|
lines.append("**Verification:**")
|
|
lines.append("```python")
|
|
lines.append(step.verification)
|
|
lines.append("```")
|
|
lines.append("")
|
|
|
|
# Common pitfall warning
|
|
if step.common_pitfall:
|
|
lines.append(f"⚠️ **Common Pitfall:** {step.common_pitfall}")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def _create_complete_example(self, guide: HowToGuide) -> str:
|
|
"""Create complete working example"""
|
|
lines = ["## Complete Example"]
|
|
lines.append("")
|
|
lines.append("```python")
|
|
|
|
# If we have workflows, use the first one's code
|
|
if guide.workflows:
|
|
workflow = guide.workflows[0]
|
|
|
|
# Add setup code if present
|
|
if workflow.get("setup_code"):
|
|
lines.append("# Setup")
|
|
lines.append(workflow["setup_code"])
|
|
lines.append("")
|
|
|
|
# Add main workflow code
|
|
lines.append("# Workflow")
|
|
lines.append(workflow.get("code", ""))
|
|
else:
|
|
# Combine all steps
|
|
for step in guide.steps:
|
|
lines.append(f"# Step {step.step_number}: {step.description}")
|
|
lines.append(step.code)
|
|
if step.verification:
|
|
lines.append(step.verification)
|
|
lines.append("")
|
|
|
|
lines.append("```")
|
|
return "\n".join(lines)
|
|
|
|
def _create_troubleshooting(self, guide: HowToGuide) -> str:
|
|
"""Create troubleshooting section"""
|
|
lines = ["## Troubleshooting"]
|
|
lines.append("")
|
|
|
|
# Common pitfalls
|
|
if guide.common_pitfalls:
|
|
lines.append("### Common Issues")
|
|
lines.append("")
|
|
for i, pitfall in enumerate(guide.common_pitfalls, 1):
|
|
lines.append(f"{i}. {pitfall}")
|
|
lines.append("")
|
|
|
|
# Specific troubleshooting
|
|
if guide.troubleshooting:
|
|
for problem, solution in guide.troubleshooting.items():
|
|
lines.append(f"### Problem: {problem}")
|
|
lines.append("")
|
|
lines.append(f"**Solution:** {solution}")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def _create_next_steps(self, guide: HowToGuide) -> str:
|
|
"""Create next steps and related guides"""
|
|
lines = ["## Next Steps"]
|
|
lines.append("")
|
|
|
|
# Variations if available
|
|
if guide.variations:
|
|
lines.append("**Try these variations:**")
|
|
for variation in guide.variations:
|
|
lines.append(f"- {variation}")
|
|
lines.append("")
|
|
|
|
# Related guides
|
|
if guide.related_guides:
|
|
lines.append("## Related Guides")
|
|
lines.append("")
|
|
for related in guide.related_guides:
|
|
lines.append(f"- [{related}]")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
def _create_footer(self, guide: HowToGuide) -> str:
|
|
"""Create guide footer with metadata"""
|
|
source_info = []
|
|
if guide.source_files:
|
|
source_info.append(f"Source: {', '.join(guide.source_files)}")
|
|
source_info.append(f"Complexity: {guide.complexity_level.title()}")
|
|
source_info.append(f"Last updated: {datetime.now().strftime('%Y-%m-%d')}")
|
|
|
|
return f"---\n\n*{' | '.join(source_info)}*"
|
|
|
|
def generate_index(self, guides: list[HowToGuide]) -> str:
|
|
"""
|
|
Generate index/TOC markdown.
|
|
|
|
Args:
|
|
guides: List of all guides
|
|
|
|
Returns:
|
|
Index markdown string
|
|
"""
|
|
lines = ["# How-To Guides Index"]
|
|
lines.append("")
|
|
lines.append(f"**Total Guides**: {len(guides)}")
|
|
lines.append(f"**Last Updated**: {datetime.now().strftime('%Y-%m-%d')}")
|
|
lines.append("")
|
|
|
|
# Group by use case
|
|
by_use_case = defaultdict(list)
|
|
for guide in guides:
|
|
use_case = guide.use_case or "Other"
|
|
by_use_case[use_case].append(guide)
|
|
|
|
lines.append("## By Use Case")
|
|
lines.append("")
|
|
|
|
for use_case in sorted(by_use_case.keys()):
|
|
case_guides = by_use_case[use_case]
|
|
lines.append(f"### {use_case} ({len(case_guides)} guides)")
|
|
for guide in sorted(case_guides, key=lambda g: g.complexity_level):
|
|
# Create filename from guide title
|
|
filename = guide.title.lower().replace(" ", "-").replace(":", "")
|
|
lines.append(
|
|
f"- [How To: {guide.title}]({use_case.lower()}/{filename}.md) - {guide.complexity_level.title()}"
|
|
)
|
|
lines.append("")
|
|
|
|
# Group by difficulty
|
|
by_complexity = defaultdict(list)
|
|
for guide in guides:
|
|
by_complexity[guide.complexity_level].append(guide)
|
|
|
|
lines.append("## By Difficulty Level")
|
|
lines.append("")
|
|
|
|
for level in ["beginner", "intermediate", "advanced"]:
|
|
if level in by_complexity:
|
|
level_guides = by_complexity[level]
|
|
lines.append(f"### {level.title()} ({len(level_guides)} guides)")
|
|
for guide in sorted(level_guides, key=lambda g: g.title):
|
|
lines.append(f"- {guide.title}")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
# ============================================================================
|
|
# HOW-TO GUIDE BUILDER (Main Orchestrator)
|
|
# ============================================================================
|
|
|
|
|
|
class HowToGuideBuilder:
|
|
"""Main orchestrator for building how-to guides from workflow examples"""
|
|
|
|
def __init__(self, enhance_with_ai: bool = True):
|
|
"""
|
|
Initialize guide builder.
|
|
|
|
Args:
|
|
enhance_with_ai: Enable AI enhancement (requires C3.6 AI analysis in workflows)
|
|
"""
|
|
self.enhance_with_ai = enhance_with_ai
|
|
self.analyzer = WorkflowAnalyzer()
|
|
self.grouper = WorkflowGrouper()
|
|
self.generator = GuideGenerator()
|
|
|
|
def build_guides_from_examples(
|
|
self,
|
|
examples: list[dict],
|
|
grouping_strategy: str = "ai-tutorial-group",
|
|
output_dir: Path | None = None,
|
|
enhance_with_ai: bool = True,
|
|
ai_mode: str = "auto",
|
|
) -> GuideCollection:
|
|
"""
|
|
Main entry point - build guides from workflow examples.
|
|
|
|
Args:
|
|
examples: List of TestExample dicts from C3.2
|
|
grouping_strategy: How to group workflows ("ai-tutorial-group", "file-path", etc.)
|
|
output_dir: Optional directory to save markdown files
|
|
enhance_with_ai: Enable comprehensive AI enhancement (default: True)
|
|
ai_mode: AI enhancement mode - "auto", "api", "local", or "none"
|
|
|
|
Returns:
|
|
GuideCollection with all generated guides
|
|
"""
|
|
logger.info(f"Building how-to guides from {len(examples)} examples...")
|
|
|
|
# Initialize AI enhancer if requested
|
|
enhancer = None
|
|
if enhance_with_ai and ai_mode != "none":
|
|
try:
|
|
from .guide_enhancer import GuideEnhancer
|
|
|
|
enhancer = GuideEnhancer(mode=ai_mode)
|
|
logger.info(f"✨ AI enhancement enabled (mode: {enhancer.mode})")
|
|
except Exception as e:
|
|
logger.warning(f"⚠️ AI enhancement unavailable: {e}")
|
|
logger.info("📝 Falling back to basic guide generation")
|
|
|
|
# Filter to workflow examples only
|
|
workflows = self._extract_workflow_examples(examples)
|
|
logger.info(f"Found {len(workflows)} workflow examples")
|
|
|
|
if not workflows:
|
|
logger.warning("No workflow examples found!")
|
|
return GuideCollection(
|
|
total_guides=0, guides_by_complexity={}, guides_by_use_case={}, guides=[]
|
|
)
|
|
|
|
# Group workflows
|
|
grouped_workflows = self.grouper.group_workflows(workflows, grouping_strategy)
|
|
logger.info(f"Grouped into {len(grouped_workflows)} guide categories")
|
|
|
|
# Build guides
|
|
guides = []
|
|
for title, workflow_group in grouped_workflows.items():
|
|
guide = self._create_guide(title, workflow_group, enhancer)
|
|
guides.append(guide)
|
|
|
|
# Create collection
|
|
collection = self._create_collection(guides)
|
|
|
|
# Save to files if output directory provided
|
|
if output_dir:
|
|
self._save_guides_to_files(collection, output_dir)
|
|
|
|
logger.info(f"✅ Generated {len(guides)} how-to guides")
|
|
return collection
|
|
|
|
def _extract_workflow_examples(self, examples: list[dict]) -> list[dict]:
|
|
"""Filter to workflow category only"""
|
|
return [ex for ex in examples if ex.get("category") == "workflow"]
|
|
|
|
def _create_guide(self, title: str, workflows: list[dict], enhancer=None) -> HowToGuide:
|
|
"""
|
|
Generate single guide from workflow(s).
|
|
|
|
Args:
|
|
title: Guide title
|
|
workflows: List of related workflow examples
|
|
enhancer: Optional GuideEnhancer instance for AI enhancement
|
|
|
|
Returns:
|
|
Complete HowToGuide object
|
|
"""
|
|
# Use first workflow as primary
|
|
primary_workflow = workflows[0]
|
|
|
|
# Analyze workflow to extract steps
|
|
steps, metadata = self.analyzer.analyze_workflow(primary_workflow)
|
|
|
|
# Generate guide ID
|
|
guide_id = hashlib.md5(title.encode()).hexdigest()[:12]
|
|
|
|
# Extract use case from AI analysis or title
|
|
use_case = title
|
|
if primary_workflow.get("ai_analysis"):
|
|
use_case = primary_workflow["ai_analysis"].get("tutorial_group", title)
|
|
|
|
# Determine overview
|
|
overview = self._generate_overview(primary_workflow, workflows)
|
|
|
|
# Extract tags
|
|
tags = primary_workflow.get("tags", [])
|
|
|
|
# Extract source files
|
|
source_files = [w.get("file_path", "") for w in workflows]
|
|
source_files = [
|
|
f"{Path(f).name}:{w.get('line_start', 0)}" for f, w in zip(source_files, workflows)
|
|
]
|
|
|
|
# Create guide
|
|
guide = HowToGuide(
|
|
guide_id=guide_id,
|
|
title=title,
|
|
overview=overview,
|
|
complexity_level=metadata.get("complexity_level", "intermediate"),
|
|
prerequisites=metadata.get("prerequisites", []),
|
|
required_imports=metadata.get("required_imports", []),
|
|
required_fixtures=metadata.get("required_fixtures", []),
|
|
workflows=workflows,
|
|
steps=steps,
|
|
use_case=use_case,
|
|
tags=tags,
|
|
estimated_time=metadata.get("estimated_time", "10 minutes"),
|
|
source_files=source_files,
|
|
)
|
|
|
|
# Add AI enhancements if enhancer is available
|
|
if enhancer:
|
|
self._enhance_guide_with_ai(guide, primary_workflow.get("ai_analysis", {}), enhancer)
|
|
elif self.enhance_with_ai and primary_workflow.get("ai_analysis"):
|
|
# Fallback to old enhancement method (basic)
|
|
self._enhance_guide_with_ai_basic(guide, primary_workflow["ai_analysis"])
|
|
|
|
return guide
|
|
|
|
def _generate_overview(self, primary_workflow: dict, _all_workflows: list[dict]) -> str:
|
|
"""Generate guide overview"""
|
|
# Try to get explanation from AI analysis
|
|
if primary_workflow.get("ai_analysis"):
|
|
explanation = primary_workflow["ai_analysis"].get("explanation")
|
|
if explanation:
|
|
return explanation
|
|
|
|
# Fallback to description
|
|
description = primary_workflow.get("description", "")
|
|
if description:
|
|
return description
|
|
|
|
# Final fallback
|
|
return f"Learn how to use {primary_workflow.get('test_name', 'this feature')} in your code."
|
|
|
|
def _enhance_guide_with_ai(self, guide: HowToGuide, _ai_analysis: dict, enhancer):
|
|
"""
|
|
Comprehensively enhance guide with AI using GuideEnhancer.
|
|
|
|
Applies all 5 enhancements:
|
|
1. Step descriptions - Natural language explanations 2. Troubleshooting - Diagnostic flows + solutions
|
|
3. Prerequisites - Why needed + setup
|
|
4. Next steps - Related guides, variations
|
|
5. Use cases - Real-world scenarios
|
|
|
|
Args:
|
|
guide: HowToGuide object to enhance
|
|
ai_analysis: AI analysis data from C3.6 (for context)
|
|
enhancer: GuideEnhancer instance
|
|
"""
|
|
# Prepare guide data for enhancer
|
|
guide_data = {
|
|
"title": guide.title,
|
|
"steps": [{"description": step.description, "code": step.code} for step in guide.steps],
|
|
"language": "python", # TODO: Detect from code
|
|
"prerequisites": guide.prerequisites,
|
|
"description": guide.overview,
|
|
}
|
|
|
|
# Call enhancer to get all 5 enhancements
|
|
enhanced_data = enhancer.enhance_guide(guide_data)
|
|
|
|
# Apply step enhancements
|
|
if "step_enhancements" in enhanced_data:
|
|
for enhancement in enhanced_data["step_enhancements"]:
|
|
idx = enhancement.step_index
|
|
if 0 <= idx < len(guide.steps):
|
|
guide.steps[idx].explanation = enhancement.explanation
|
|
guide.steps[idx].common_variations = enhancement.variations
|
|
|
|
# Apply detailed prerequisites
|
|
if "prerequisites_detailed" in enhanced_data:
|
|
guide.prerequisites_detailed = enhanced_data["prerequisites_detailed"]
|
|
|
|
# Apply troubleshooting
|
|
if "troubleshooting_detailed" in enhanced_data:
|
|
guide.troubleshooting_detailed = enhanced_data["troubleshooting_detailed"]
|
|
|
|
# Apply next steps
|
|
if "next_steps_detailed" in enhanced_data:
|
|
guide.next_steps_detailed = enhanced_data["next_steps_detailed"]
|
|
|
|
# Apply use cases
|
|
if "use_cases" in enhanced_data:
|
|
guide.use_cases = enhanced_data["use_cases"]
|
|
|
|
logger.info(f"✨ Enhanced guide '{guide.title}' with comprehensive AI improvements")
|
|
|
|
def _enhance_guide_with_ai_basic(self, guide: HowToGuide, ai_analysis: dict):
|
|
"""
|
|
Basic enhancement using pre-computed AI analysis from C3.6.
|
|
|
|
This is a fallback when GuideEnhancer is not available.
|
|
|
|
Args:
|
|
guide: HowToGuide object to enhance
|
|
ai_analysis: AI analysis data from C3.6
|
|
"""
|
|
# Add best practices as variations
|
|
best_practices = ai_analysis.get("best_practices", [])
|
|
guide.variations = best_practices
|
|
|
|
# Add common mistakes as pitfalls
|
|
common_mistakes = ai_analysis.get("common_mistakes", [])
|
|
guide.common_pitfalls = common_mistakes
|
|
|
|
# Add related examples as related guides
|
|
related_examples = ai_analysis.get("related_examples", [])
|
|
guide.related_guides = [f"How To: {ex}" for ex in related_examples]
|
|
|
|
# Enhance step explanations
|
|
for step in guide.steps:
|
|
# Add explanation to steps based on best practices
|
|
if best_practices and step.step_number <= len(best_practices):
|
|
step.explanation = best_practices[step.step_number - 1]
|
|
|
|
def _create_collection(self, guides: list[HowToGuide]) -> GuideCollection:
|
|
"""Create GuideCollection from guides"""
|
|
# Count by complexity
|
|
by_complexity = defaultdict(int)
|
|
for guide in guides:
|
|
by_complexity[guide.complexity_level] += 1
|
|
|
|
# Group by use case
|
|
by_use_case = defaultdict(list)
|
|
for guide in guides:
|
|
use_case = guide.use_case or "Other"
|
|
by_use_case[use_case].append(guide)
|
|
|
|
return GuideCollection(
|
|
total_guides=len(guides),
|
|
guides_by_complexity=dict(by_complexity),
|
|
guides_by_use_case=dict(by_use_case),
|
|
guides=guides,
|
|
)
|
|
|
|
def _save_guides_to_files(self, collection: GuideCollection, output_dir: Path):
|
|
"""Save guides to markdown files"""
|
|
output_dir = Path(output_dir)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
logger.info(f"Saving guides to {output_dir}...")
|
|
|
|
# Save individual guides
|
|
for use_case, guides in collection.guides_by_use_case.items():
|
|
# Create use case directory
|
|
use_case_dir = output_dir / use_case.lower().replace(" ", "-")
|
|
use_case_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
for guide in guides:
|
|
# Generate filename from title
|
|
filename = guide.title.lower().replace(" ", "-").replace(":", "") + ".md"
|
|
file_path = use_case_dir / filename
|
|
|
|
# Generate and save markdown
|
|
markdown = self.generator.generate_guide_markdown(guide)
|
|
file_path.write_text(markdown, encoding="utf-8")
|
|
|
|
# Save index
|
|
index_markdown = self.generator.generate_index(collection.guides)
|
|
(output_dir / "index.md").write_text(index_markdown, encoding="utf-8")
|
|
|
|
logger.info(f"✅ Saved {collection.total_guides} guides + index to {output_dir}")
|
|
|
|
|
|
# ============================================================================
|
|
# CLI INTERFACE
|
|
# ============================================================================
|
|
|
|
|
|
def main():
|
|
"""CLI entry point for how-to guide builder"""
|
|
import argparse
|
|
import sys
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Build how-to guides from workflow test examples (C3.3)",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
# From test examples JSON (C3.2 output)
|
|
skill-seekers build-how-to-guides --input test_examples.json
|
|
|
|
# From directory (extracts workflows)
|
|
skill-seekers build-how-to-guides tests/
|
|
|
|
# Custom grouping strategy
|
|
skill-seekers build-how-to-guides tests/ --group-by file-path
|
|
|
|
# Custom output directory
|
|
skill-seekers build-how-to-guides tests/ --output tutorials/
|
|
|
|
# Without AI enhancement
|
|
skill-seekers build-how-to-guides tests/ --no-ai
|
|
|
|
Grouping Strategies:
|
|
- ai-tutorial-group: Use AI-generated tutorial groups (default, best)
|
|
- file-path: Group by source test file
|
|
- test-name: Group by test name patterns
|
|
- complexity: Group by difficulty level
|
|
""",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"input", nargs="?", help="Input: directory with test files OR test_examples.json file"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--input", dest="input_file", help="Input JSON file with test examples (from C3.2)"
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--output",
|
|
default="output/codebase/tutorials",
|
|
help="Output directory for generated guides (default: output/codebase/tutorials)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--group-by",
|
|
choices=["ai-tutorial-group", "file-path", "test-name", "complexity"],
|
|
default="ai-tutorial-group",
|
|
help="Grouping strategy (default: ai-tutorial-group)",
|
|
)
|
|
|
|
parser.add_argument("--no-ai", action="store_true", help="Disable AI enhancement")
|
|
|
|
parser.add_argument(
|
|
"--json-output", action="store_true", help="Output JSON summary instead of markdown files"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Determine input source
|
|
input_path = args.input or args.input_file
|
|
|
|
if not input_path:
|
|
parser.print_help()
|
|
print("\n❌ Error: No input provided")
|
|
print(" Provide either a directory or --input JSON file")
|
|
sys.exit(1)
|
|
|
|
input_path = Path(input_path)
|
|
|
|
# Load examples
|
|
examples = []
|
|
|
|
if input_path.is_file() and input_path.suffix == ".json":
|
|
# Load from JSON file
|
|
logger.info(f"Loading examples from {input_path}...")
|
|
with open(input_path) as f:
|
|
data = json.load(f)
|
|
if isinstance(data, dict) and "examples" in data:
|
|
examples = data["examples"]
|
|
elif isinstance(data, list):
|
|
examples = data
|
|
else:
|
|
print(f"❌ Error: Invalid JSON format in {input_path}")
|
|
sys.exit(1)
|
|
|
|
elif input_path.is_dir():
|
|
# Extract from directory using test example extractor
|
|
print("⚠️ Directory input requires test example extractor")
|
|
print(" Please use test_examples.json output from C3.2")
|
|
print(f" Or run: skill-seekers extract-test-examples {input_path} --json > examples.json")
|
|
sys.exit(1)
|
|
|
|
else:
|
|
print(f"❌ Error: Input path not found: {input_path}")
|
|
sys.exit(1)
|
|
|
|
# Build guides
|
|
builder = HowToGuideBuilder(enhance_with_ai=not args.no_ai)
|
|
output_dir = Path(args.output) if not args.json_output else None
|
|
|
|
collection = builder.build_guides_from_examples(
|
|
examples, grouping_strategy=args.group_by, output_dir=output_dir
|
|
)
|
|
|
|
# Output results
|
|
if args.json_output:
|
|
# JSON output
|
|
print(json.dumps(collection.to_dict(), indent=2))
|
|
else:
|
|
# Summary
|
|
print()
|
|
print("=" * 60)
|
|
print("HOW-TO GUIDES GENERATED")
|
|
print("=" * 60)
|
|
print()
|
|
print(f"Total Guides: {collection.total_guides}")
|
|
print()
|
|
print("By Complexity:")
|
|
for level, count in collection.guides_by_complexity.items():
|
|
print(f" - {level.title()}: {count} guides")
|
|
print()
|
|
print("By Use Case:")
|
|
for use_case, guides in collection.guides_by_use_case.items():
|
|
print(f" - {use_case}: {len(guides)} guides")
|
|
print()
|
|
if output_dir:
|
|
print(f"📁 Output directory: {output_dir}")
|
|
print(f"📄 Index file: {output_dir}/index.md")
|
|
print()
|
|
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|