skill-seekers 2.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- skill_seekers/__init__.py +22 -0
- skill_seekers/cli/__init__.py +39 -0
- skill_seekers/cli/adaptors/__init__.py +120 -0
- skill_seekers/cli/adaptors/base.py +221 -0
- skill_seekers/cli/adaptors/claude.py +485 -0
- skill_seekers/cli/adaptors/gemini.py +453 -0
- skill_seekers/cli/adaptors/markdown.py +269 -0
- skill_seekers/cli/adaptors/openai.py +503 -0
- skill_seekers/cli/ai_enhancer.py +310 -0
- skill_seekers/cli/api_reference_builder.py +373 -0
- skill_seekers/cli/architectural_pattern_detector.py +525 -0
- skill_seekers/cli/code_analyzer.py +1462 -0
- skill_seekers/cli/codebase_scraper.py +1225 -0
- skill_seekers/cli/config_command.py +563 -0
- skill_seekers/cli/config_enhancer.py +431 -0
- skill_seekers/cli/config_extractor.py +871 -0
- skill_seekers/cli/config_manager.py +452 -0
- skill_seekers/cli/config_validator.py +394 -0
- skill_seekers/cli/conflict_detector.py +528 -0
- skill_seekers/cli/constants.py +72 -0
- skill_seekers/cli/dependency_analyzer.py +757 -0
- skill_seekers/cli/doc_scraper.py +2332 -0
- skill_seekers/cli/enhance_skill.py +488 -0
- skill_seekers/cli/enhance_skill_local.py +1096 -0
- skill_seekers/cli/enhance_status.py +194 -0
- skill_seekers/cli/estimate_pages.py +433 -0
- skill_seekers/cli/generate_router.py +1209 -0
- skill_seekers/cli/github_fetcher.py +534 -0
- skill_seekers/cli/github_scraper.py +1466 -0
- skill_seekers/cli/guide_enhancer.py +723 -0
- skill_seekers/cli/how_to_guide_builder.py +1267 -0
- skill_seekers/cli/install_agent.py +461 -0
- skill_seekers/cli/install_skill.py +178 -0
- skill_seekers/cli/language_detector.py +614 -0
- skill_seekers/cli/llms_txt_detector.py +60 -0
- skill_seekers/cli/llms_txt_downloader.py +104 -0
- skill_seekers/cli/llms_txt_parser.py +150 -0
- skill_seekers/cli/main.py +558 -0
- skill_seekers/cli/markdown_cleaner.py +132 -0
- skill_seekers/cli/merge_sources.py +806 -0
- skill_seekers/cli/package_multi.py +77 -0
- skill_seekers/cli/package_skill.py +241 -0
- skill_seekers/cli/pattern_recognizer.py +1825 -0
- skill_seekers/cli/pdf_extractor_poc.py +1166 -0
- skill_seekers/cli/pdf_scraper.py +617 -0
- skill_seekers/cli/quality_checker.py +519 -0
- skill_seekers/cli/rate_limit_handler.py +438 -0
- skill_seekers/cli/resume_command.py +160 -0
- skill_seekers/cli/run_tests.py +230 -0
- skill_seekers/cli/setup_wizard.py +93 -0
- skill_seekers/cli/split_config.py +390 -0
- skill_seekers/cli/swift_patterns.py +560 -0
- skill_seekers/cli/test_example_extractor.py +1081 -0
- skill_seekers/cli/test_unified_simple.py +179 -0
- skill_seekers/cli/unified_codebase_analyzer.py +572 -0
- skill_seekers/cli/unified_scraper.py +932 -0
- skill_seekers/cli/unified_skill_builder.py +1605 -0
- skill_seekers/cli/upload_skill.py +162 -0
- skill_seekers/cli/utils.py +432 -0
- skill_seekers/mcp/__init__.py +33 -0
- skill_seekers/mcp/agent_detector.py +316 -0
- skill_seekers/mcp/git_repo.py +273 -0
- skill_seekers/mcp/server.py +231 -0
- skill_seekers/mcp/server_fastmcp.py +1249 -0
- skill_seekers/mcp/server_legacy.py +2302 -0
- skill_seekers/mcp/source_manager.py +285 -0
- skill_seekers/mcp/tools/__init__.py +115 -0
- skill_seekers/mcp/tools/config_tools.py +251 -0
- skill_seekers/mcp/tools/packaging_tools.py +826 -0
- skill_seekers/mcp/tools/scraping_tools.py +842 -0
- skill_seekers/mcp/tools/source_tools.py +828 -0
- skill_seekers/mcp/tools/splitting_tools.py +212 -0
- skill_seekers/py.typed +0 -0
- skill_seekers-2.7.3.dist-info/METADATA +2027 -0
- skill_seekers-2.7.3.dist-info/RECORD +79 -0
- skill_seekers-2.7.3.dist-info/WHEEL +5 -0
- skill_seekers-2.7.3.dist-info/entry_points.txt +19 -0
- skill_seekers-2.7.3.dist-info/licenses/LICENSE +21 -0
- skill_seekers-2.7.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
How-To Guide Builder (C3.3) - Build step-by-step guides from workflow examples
|
|
4
|
+
|
|
5
|
+
Transforms multi-step test workflows (from C3.2) into educational "how to" guides with:
|
|
6
|
+
- Step-by-step breakdowns
|
|
7
|
+
- Prerequisites and setup requirements
|
|
8
|
+
- Verification checkpoints
|
|
9
|
+
- Troubleshooting sections
|
|
10
|
+
- Complexity levels (beginner/intermediate/advanced)
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
# From test examples JSON
|
|
14
|
+
skill-seekers build-how-to-guides --input test_examples.json
|
|
15
|
+
|
|
16
|
+
# From directory (auto-extracts workflows)
|
|
17
|
+
skill-seekers build-how-to-guides tests/
|
|
18
|
+
|
|
19
|
+
# With AI enhancement
|
|
20
|
+
skill-seekers build-how-to-guides tests/ --enhance-with-ai
|
|
21
|
+
|
|
22
|
+
Example workflow → guide transformation:
|
|
23
|
+
Input: Multi-step test showing user registration + login + session
|
|
24
|
+
Output: "How To: Complete User Authentication" guide with:
|
|
25
|
+
- 5 discrete steps with explanations
|
|
26
|
+
- Prerequisites (database, email service)
|
|
27
|
+
- Verification points at each step
|
|
28
|
+
- Common pitfalls and troubleshooting
|
|
29
|
+
- Related guides suggestions
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import ast
|
|
33
|
+
import hashlib
|
|
34
|
+
import json
|
|
35
|
+
import logging
|
|
36
|
+
import re
|
|
37
|
+
from collections import defaultdict
|
|
38
|
+
from dataclasses import asdict, dataclass, field
|
|
39
|
+
from datetime import datetime
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Literal
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ============================================================================
|
|
47
|
+
# DATA MODELS
|
|
48
|
+
# ============================================================================
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class PrerequisiteItem:
|
|
53
|
+
"""Enhanced prerequisite with explanation (AI enhancement)"""
|
|
54
|
+
|
|
55
|
+
name: str
|
|
56
|
+
why: str # Why this is needed
|
|
57
|
+
setup: str # How to install/configure
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class TroubleshootingItem:
|
|
62
|
+
"""Enhanced troubleshooting with solutions (AI enhancement)"""
|
|
63
|
+
|
|
64
|
+
problem: str
|
|
65
|
+
symptoms: list[str] = field(default_factory=list) # How to recognize this issue
|
|
66
|
+
solution: str = "" # Step-by-step fix
|
|
67
|
+
diagnostic_steps: list[str] = field(default_factory=list) # How to diagnose
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class WorkflowStep:
|
|
72
|
+
"""Single step in a workflow guide"""
|
|
73
|
+
|
|
74
|
+
step_number: int
|
|
75
|
+
code: str
|
|
76
|
+
description: str
|
|
77
|
+
expected_result: str | None = None
|
|
78
|
+
verification: str | None = None # Assertion or checkpoint
|
|
79
|
+
setup_required: str | None = None
|
|
80
|
+
explanation: str | None = None # Why this step matters
|
|
81
|
+
common_pitfall: str | None = None # Warning for this step
|
|
82
|
+
common_variations: list[str] = field(default_factory=list) # AI: Alternative approaches
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class HowToGuide:
|
|
87
|
+
"""Complete how-to guide generated from workflow(s)"""
|
|
88
|
+
|
|
89
|
+
guide_id: str
|
|
90
|
+
title: str
|
|
91
|
+
overview: str
|
|
92
|
+
complexity_level: Literal["beginner", "intermediate", "advanced"]
|
|
93
|
+
|
|
94
|
+
# Prerequisites
|
|
95
|
+
prerequisites: list[str] = field(default_factory=list)
|
|
96
|
+
required_imports: list[str] = field(default_factory=list)
|
|
97
|
+
required_fixtures: list[str] = field(default_factory=list)
|
|
98
|
+
|
|
99
|
+
# Content
|
|
100
|
+
workflows: list[dict] = field(default_factory=list) # Source workflow examples
|
|
101
|
+
steps: list[WorkflowStep] = field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
# Metadata
|
|
104
|
+
use_case: str = ""
|
|
105
|
+
tags: list[str] = field(default_factory=list)
|
|
106
|
+
estimated_time: str = "10 minutes"
|
|
107
|
+
source_files: list[str] = field(default_factory=list)
|
|
108
|
+
|
|
109
|
+
# Optional AI enhancement (basic)
|
|
110
|
+
common_pitfalls: list[str] = field(default_factory=list)
|
|
111
|
+
troubleshooting: dict[str, str] = field(default_factory=dict)
|
|
112
|
+
variations: list[str] = field(default_factory=list)
|
|
113
|
+
related_guides: list[str] = field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
# AI enhancement (comprehensive - NEW)
|
|
116
|
+
prerequisites_detailed: list[PrerequisiteItem] = field(default_factory=list)
|
|
117
|
+
troubleshooting_detailed: list[TroubleshootingItem] = field(default_factory=list)
|
|
118
|
+
next_steps_detailed: list[str] = field(default_factory=list)
|
|
119
|
+
use_cases: list[str] = field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
def to_dict(self) -> dict:
|
|
122
|
+
"""Convert to dictionary"""
|
|
123
|
+
result = asdict(self)
|
|
124
|
+
# Convert WorkflowStep objects to dicts
|
|
125
|
+
result["steps"] = [asdict(step) for step in self.steps]
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class GuideCollection:
|
|
131
|
+
"""Collection of guides organized by category"""
|
|
132
|
+
|
|
133
|
+
total_guides: int
|
|
134
|
+
guides_by_complexity: dict[str, int]
|
|
135
|
+
guides_by_use_case: dict[str, list[HowToGuide]]
|
|
136
|
+
guides: list[HowToGuide]
|
|
137
|
+
|
|
138
|
+
def to_dict(self) -> dict:
|
|
139
|
+
"""Convert to dictionary"""
|
|
140
|
+
return {
|
|
141
|
+
"total_guides": self.total_guides,
|
|
142
|
+
"guides_by_complexity": self.guides_by_complexity,
|
|
143
|
+
"guides_by_use_case": {
|
|
144
|
+
k: [g.to_dict() for g in v] for k, v in self.guides_by_use_case.items()
|
|
145
|
+
},
|
|
146
|
+
"guides": [g.to_dict() for g in self.guides],
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ============================================================================
|
|
151
|
+
# WORKFLOW ANALYZER
|
|
152
|
+
# ============================================================================
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class WorkflowAnalyzer:
|
|
156
|
+
"""Analyze workflow examples to extract steps and metadata"""
|
|
157
|
+
|
|
158
|
+
def analyze_workflow(self, workflow: dict) -> tuple[list[WorkflowStep], dict]:
|
|
159
|
+
"""
|
|
160
|
+
Deep analysis of workflow structure.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
workflow: TestExample dict from C3.2
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
(steps, metadata) where metadata includes prerequisites, complexity, etc.
|
|
167
|
+
"""
|
|
168
|
+
code = workflow.get("code", "")
|
|
169
|
+
language = workflow.get("language", "python").lower()
|
|
170
|
+
|
|
171
|
+
# Extract steps based on language
|
|
172
|
+
if language == "python":
|
|
173
|
+
steps = self._extract_steps_python(code, workflow)
|
|
174
|
+
else:
|
|
175
|
+
steps = self._extract_steps_heuristic(code, workflow)
|
|
176
|
+
|
|
177
|
+
# Detect prerequisites
|
|
178
|
+
metadata = self._detect_prerequisites(workflow)
|
|
179
|
+
|
|
180
|
+
# Find verification points
|
|
181
|
+
verifications = self._find_verification_points(code)
|
|
182
|
+
|
|
183
|
+
# Associate verifications with steps
|
|
184
|
+
for i, step in enumerate(steps):
|
|
185
|
+
if i < len(verifications):
|
|
186
|
+
step.verification = verifications[i]
|
|
187
|
+
|
|
188
|
+
# Calculate complexity
|
|
189
|
+
metadata["complexity_level"] = self._calculate_complexity(steps, workflow)
|
|
190
|
+
metadata["estimated_time"] = self._estimate_time(steps)
|
|
191
|
+
|
|
192
|
+
return steps, metadata
|
|
193
|
+
|
|
194
|
+
def _extract_steps_python(self, code: str, workflow: dict) -> list[WorkflowStep]:
|
|
195
|
+
"""Extract steps from Python code using AST"""
|
|
196
|
+
steps = []
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
tree = ast.parse(code)
|
|
200
|
+
statements = []
|
|
201
|
+
|
|
202
|
+
# Collect all statements
|
|
203
|
+
for node in ast.walk(tree):
|
|
204
|
+
if isinstance(node, (ast.Assign, ast.Expr, ast.Assert)):
|
|
205
|
+
statements.append(node)
|
|
206
|
+
|
|
207
|
+
step_num = 1
|
|
208
|
+
for stmt in statements:
|
|
209
|
+
# Skip assertions for now (they're verifications)
|
|
210
|
+
if isinstance(stmt, ast.Assert):
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
# Get code for this statement
|
|
214
|
+
step_code = ast.get_source_segment(code, stmt)
|
|
215
|
+
if not step_code:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
# Generate description from code
|
|
219
|
+
description = self._generate_step_description(stmt, step_code)
|
|
220
|
+
|
|
221
|
+
# Check if next statement is assertion (verification)
|
|
222
|
+
idx = statements.index(stmt)
|
|
223
|
+
verification = None
|
|
224
|
+
if idx + 1 < len(statements) and isinstance(statements[idx + 1], ast.Assert):
|
|
225
|
+
verification = ast.get_source_segment(code, statements[idx + 1])
|
|
226
|
+
|
|
227
|
+
steps.append(
|
|
228
|
+
WorkflowStep(
|
|
229
|
+
step_number=step_num,
|
|
230
|
+
code=step_code,
|
|
231
|
+
description=description,
|
|
232
|
+
verification=verification,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
step_num += 1
|
|
236
|
+
|
|
237
|
+
except SyntaxError:
|
|
238
|
+
# Fall back to heuristic method
|
|
239
|
+
return self._extract_steps_heuristic(code, workflow)
|
|
240
|
+
|
|
241
|
+
return steps
|
|
242
|
+
|
|
243
|
+
def _extract_steps_heuristic(self, code: str, _workflow: dict) -> list[WorkflowStep]:
|
|
244
|
+
"""Extract steps using heuristics (for non-Python or invalid syntax)"""
|
|
245
|
+
steps = []
|
|
246
|
+
lines = code.split("\n")
|
|
247
|
+
|
|
248
|
+
current_step = []
|
|
249
|
+
step_num = 1
|
|
250
|
+
|
|
251
|
+
for line in lines:
|
|
252
|
+
line_stripped = line.strip()
|
|
253
|
+
|
|
254
|
+
# Skip empty lines and comments
|
|
255
|
+
if not line_stripped or line_stripped.startswith("#"):
|
|
256
|
+
if current_step:
|
|
257
|
+
# End of current step
|
|
258
|
+
step_code = "\n".join(current_step)
|
|
259
|
+
description = self._infer_description_from_code(step_code)
|
|
260
|
+
|
|
261
|
+
steps.append(
|
|
262
|
+
WorkflowStep(
|
|
263
|
+
step_number=step_num,
|
|
264
|
+
code=step_code,
|
|
265
|
+
description=description,
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
step_num += 1
|
|
269
|
+
current_step = []
|
|
270
|
+
continue
|
|
271
|
+
|
|
272
|
+
current_step.append(line)
|
|
273
|
+
|
|
274
|
+
# Add final step
|
|
275
|
+
if current_step:
|
|
276
|
+
step_code = "\n".join(current_step)
|
|
277
|
+
description = self._infer_description_from_code(step_code)
|
|
278
|
+
steps.append(
|
|
279
|
+
WorkflowStep(step_number=step_num, code=step_code, description=description)
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return steps
|
|
283
|
+
|
|
284
|
+
def _generate_step_description(self, node: ast.AST, code: str) -> str:
|
|
285
|
+
"""Generate human-readable description from AST node"""
|
|
286
|
+
if isinstance(node, ast.Assign):
|
|
287
|
+
targets = [self._get_name(t) for t in node.targets]
|
|
288
|
+
value_desc = self._describe_value(node.value)
|
|
289
|
+
return f"Assign {', '.join(targets)} = {value_desc}"
|
|
290
|
+
|
|
291
|
+
elif isinstance(node, ast.Expr):
|
|
292
|
+
if isinstance(node.value, ast.Call):
|
|
293
|
+
func_name = self._get_name(node.value.func)
|
|
294
|
+
return f"Call {func_name}()"
|
|
295
|
+
|
|
296
|
+
return code.split("\n")[0] # First line as fallback
|
|
297
|
+
|
|
298
|
+
def _describe_value(self, node: ast.AST) -> str:
|
|
299
|
+
"""Describe AST value node"""
|
|
300
|
+
if isinstance(node, ast.Call):
|
|
301
|
+
func_name = self._get_name(node.func)
|
|
302
|
+
return f"{func_name}(...)"
|
|
303
|
+
elif isinstance(node, ast.Constant):
|
|
304
|
+
return repr(node.value)
|
|
305
|
+
elif isinstance(node, ast.Name):
|
|
306
|
+
return node.id
|
|
307
|
+
return "value"
|
|
308
|
+
|
|
309
|
+
def _get_name(self, node: ast.AST) -> str:
|
|
310
|
+
"""Extract name from AST node"""
|
|
311
|
+
if isinstance(node, ast.Name):
|
|
312
|
+
return node.id
|
|
313
|
+
elif isinstance(node, ast.Attribute):
|
|
314
|
+
return f"{self._get_name(node.value)}.{node.attr}"
|
|
315
|
+
elif isinstance(node, ast.Call):
|
|
316
|
+
return self._get_name(node.func)
|
|
317
|
+
return "unknown"
|
|
318
|
+
|
|
319
|
+
def _infer_description_from_code(self, code: str) -> str:
|
|
320
|
+
"""Infer description from code using patterns"""
|
|
321
|
+
code = code.strip()
|
|
322
|
+
|
|
323
|
+
# Method call patterns
|
|
324
|
+
if "(" in code and ")" in code:
|
|
325
|
+
match = re.search(r"(\w+)\s*\(", code)
|
|
326
|
+
if match:
|
|
327
|
+
return f"Call {match.group(1)}()"
|
|
328
|
+
|
|
329
|
+
# Assignment patterns
|
|
330
|
+
if "=" in code and not code.startswith("assert"):
|
|
331
|
+
parts = code.split("=", 1)
|
|
332
|
+
var_name = parts[0].strip()
|
|
333
|
+
return f"Create {var_name}"
|
|
334
|
+
|
|
335
|
+
# Assertion patterns
|
|
336
|
+
if code.startswith("assert"):
|
|
337
|
+
return "Verify result"
|
|
338
|
+
|
|
339
|
+
return code.split("\n")[0] # First line
|
|
340
|
+
|
|
341
|
+
def _detect_prerequisites(self, workflow: dict) -> dict:
|
|
342
|
+
"""Detect prerequisites from workflow"""
|
|
343
|
+
metadata = {
|
|
344
|
+
"prerequisites": [],
|
|
345
|
+
"required_imports": [],
|
|
346
|
+
"required_fixtures": [],
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# Get dependencies from workflow
|
|
350
|
+
dependencies = workflow.get("dependencies", [])
|
|
351
|
+
metadata["required_imports"] = dependencies
|
|
352
|
+
|
|
353
|
+
# Get setup code
|
|
354
|
+
setup_code = workflow.get("setup_code")
|
|
355
|
+
if setup_code:
|
|
356
|
+
metadata["prerequisites"].append("Setup code must be executed first")
|
|
357
|
+
|
|
358
|
+
# Check for common fixtures in test name or setup
|
|
359
|
+
test_name = workflow.get("test_name", "").lower()
|
|
360
|
+
if "database" in test_name or (setup_code and "database" in setup_code.lower()):
|
|
361
|
+
metadata["required_fixtures"].append("database")
|
|
362
|
+
if "api" in test_name or (setup_code and "api" in setup_code.lower()):
|
|
363
|
+
metadata["required_fixtures"].append("api_client")
|
|
364
|
+
|
|
365
|
+
return metadata
|
|
366
|
+
|
|
367
|
+
def _find_verification_points(self, code: str) -> list[str]:
|
|
368
|
+
"""Find assertion statements in code"""
|
|
369
|
+
verifications = []
|
|
370
|
+
|
|
371
|
+
for line in code.split("\n"):
|
|
372
|
+
line_stripped = line.strip()
|
|
373
|
+
if line_stripped.startswith("assert"):
|
|
374
|
+
verifications.append(line_stripped)
|
|
375
|
+
|
|
376
|
+
return verifications
|
|
377
|
+
|
|
378
|
+
def _calculate_complexity(self, steps: list[WorkflowStep], workflow: dict) -> str:
|
|
379
|
+
"""Calculate complexity level"""
|
|
380
|
+
num_steps = len(steps)
|
|
381
|
+
|
|
382
|
+
# Check for advanced patterns
|
|
383
|
+
code = workflow.get("code", "")
|
|
384
|
+
has_async = "async" in code or "await" in code
|
|
385
|
+
has_mock = "mock" in code.lower() or "patch" in code.lower()
|
|
386
|
+
has_error_handling = "try" in code or "except" in code
|
|
387
|
+
|
|
388
|
+
_complexity_score = workflow.get("complexity_score", 0.5)
|
|
389
|
+
|
|
390
|
+
# Determine level
|
|
391
|
+
if num_steps <= 3 and not has_async and not has_mock:
|
|
392
|
+
return "beginner"
|
|
393
|
+
elif num_steps >= 8 or has_async or has_error_handling:
|
|
394
|
+
return "advanced"
|
|
395
|
+
else:
|
|
396
|
+
return "intermediate"
|
|
397
|
+
|
|
398
|
+
def _estimate_time(self, steps: list[WorkflowStep]) -> str:
|
|
399
|
+
"""Estimate time to complete guide"""
|
|
400
|
+
num_steps = len(steps)
|
|
401
|
+
|
|
402
|
+
if num_steps <= 3:
|
|
403
|
+
return "5 minutes"
|
|
404
|
+
elif num_steps <= 6:
|
|
405
|
+
return "10 minutes"
|
|
406
|
+
elif num_steps <= 10:
|
|
407
|
+
return "15 minutes"
|
|
408
|
+
else:
|
|
409
|
+
return "20 minutes"
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# ============================================================================
|
|
413
|
+
# WORKFLOW GROUPER
|
|
414
|
+
# ============================================================================
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class WorkflowGrouper:
|
|
418
|
+
"""Group related workflows into coherent guides"""
|
|
419
|
+
|
|
420
|
+
def group_workflows(
|
|
421
|
+
self, workflows: list[dict], strategy: str = "ai-tutorial-group"
|
|
422
|
+
) -> dict[str, list[dict]]:
|
|
423
|
+
"""
|
|
424
|
+
Group workflows using specified strategy.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
workflows: List of workflow examples
|
|
428
|
+
strategy: "ai-tutorial-group", "file-path", "test-name", "complexity"
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Dict mapping group name to list of workflows
|
|
432
|
+
"""
|
|
433
|
+
if strategy == "ai-tutorial-group":
|
|
434
|
+
return self._group_by_ai_tutorial_group(workflows)
|
|
435
|
+
elif strategy == "file-path":
|
|
436
|
+
return self._group_by_file_path(workflows)
|
|
437
|
+
elif strategy == "test-name":
|
|
438
|
+
return self._group_by_test_name(workflows)
|
|
439
|
+
elif strategy == "complexity":
|
|
440
|
+
return self._group_by_complexity(workflows)
|
|
441
|
+
else:
|
|
442
|
+
# Default: AI tutorial group with fallback
|
|
443
|
+
groups = self._group_by_ai_tutorial_group(workflows)
|
|
444
|
+
if not groups or len(groups) == len(workflows):
|
|
445
|
+
# Fallback to file path if AI grouping didn't work well
|
|
446
|
+
groups = self._group_by_file_path(workflows)
|
|
447
|
+
return groups
|
|
448
|
+
|
|
449
|
+
def _group_by_ai_tutorial_group(self, workflows: list[dict]) -> dict[str, list[dict]]:
|
|
450
|
+
"""Group by AI-generated tutorial_group (from C3.6 enhancement)"""
|
|
451
|
+
groups = defaultdict(list)
|
|
452
|
+
ungrouped = []
|
|
453
|
+
|
|
454
|
+
for workflow in workflows:
|
|
455
|
+
ai_analysis = workflow.get("ai_analysis", {})
|
|
456
|
+
tutorial_group = ai_analysis.get("tutorial_group")
|
|
457
|
+
|
|
458
|
+
if tutorial_group:
|
|
459
|
+
groups[tutorial_group].append(workflow)
|
|
460
|
+
else:
|
|
461
|
+
ungrouped.append(workflow)
|
|
462
|
+
|
|
463
|
+
# Put ungrouped workflows in individual guides
|
|
464
|
+
for workflow in ungrouped:
|
|
465
|
+
test_name = workflow.get("test_name", "Unknown")
|
|
466
|
+
# Clean test name for title
|
|
467
|
+
title = self._clean_test_name(test_name)
|
|
468
|
+
groups[title] = [workflow]
|
|
469
|
+
|
|
470
|
+
return dict(groups)
|
|
471
|
+
|
|
472
|
+
def _group_by_file_path(self, workflows: list[dict]) -> dict[str, list[dict]]:
|
|
473
|
+
"""Group workflows from same test file"""
|
|
474
|
+
groups = defaultdict(list)
|
|
475
|
+
|
|
476
|
+
for workflow in workflows:
|
|
477
|
+
file_path = workflow.get("file_path", "")
|
|
478
|
+
# Extract meaningful name from file path
|
|
479
|
+
file_name = Path(file_path).stem if file_path else "Unknown"
|
|
480
|
+
# Remove test_ prefix
|
|
481
|
+
group_name = file_name.replace("test_", "").replace("_", " ").title()
|
|
482
|
+
groups[group_name].append(workflow)
|
|
483
|
+
|
|
484
|
+
return dict(groups)
|
|
485
|
+
|
|
486
|
+
def _group_by_test_name(self, workflows: list[dict]) -> dict[str, list[dict]]:
|
|
487
|
+
"""Group by common test name prefixes"""
|
|
488
|
+
groups = defaultdict(list)
|
|
489
|
+
|
|
490
|
+
for workflow in workflows:
|
|
491
|
+
test_name = workflow.get("test_name", "")
|
|
492
|
+
# Extract prefix (e.g., test_auth_login → auth)
|
|
493
|
+
prefix = self._extract_prefix(test_name)
|
|
494
|
+
groups[prefix].append(workflow)
|
|
495
|
+
|
|
496
|
+
return dict(groups)
|
|
497
|
+
|
|
498
|
+
def _group_by_complexity(self, workflows: list[dict]) -> dict[str, list[dict]]:
|
|
499
|
+
"""Group by complexity level"""
|
|
500
|
+
groups = {"Beginner": [], "Intermediate": [], "Advanced": []}
|
|
501
|
+
|
|
502
|
+
for workflow in workflows:
|
|
503
|
+
complexity_score = workflow.get("complexity_score", 0.5)
|
|
504
|
+
|
|
505
|
+
if complexity_score < 0.4:
|
|
506
|
+
groups["Beginner"].append(workflow)
|
|
507
|
+
elif complexity_score < 0.7:
|
|
508
|
+
groups["Intermediate"].append(workflow)
|
|
509
|
+
else:
|
|
510
|
+
groups["Advanced"].append(workflow)
|
|
511
|
+
|
|
512
|
+
# Remove empty groups
|
|
513
|
+
return {k: v for k, v in groups.items() if v}
|
|
514
|
+
|
|
515
|
+
def _clean_test_name(self, test_name: str) -> str:
|
|
516
|
+
"""Clean test name to readable title"""
|
|
517
|
+
# Remove test_ prefix
|
|
518
|
+
name = test_name.replace("test_", "")
|
|
519
|
+
# Replace underscores with spaces
|
|
520
|
+
name = name.replace("_", " ")
|
|
521
|
+
# Title case
|
|
522
|
+
return name.title()
|
|
523
|
+
|
|
524
|
+
def _extract_prefix(self, test_name: str) -> str:
|
|
525
|
+
"""Extract prefix from test name"""
|
|
526
|
+
# Remove test_ prefix
|
|
527
|
+
name = test_name.replace("test_", "")
|
|
528
|
+
# Get first part before underscore
|
|
529
|
+
parts = name.split("_")
|
|
530
|
+
if len(parts) > 1:
|
|
531
|
+
return parts[0].title()
|
|
532
|
+
return self._clean_test_name(test_name)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ============================================================================
|
|
536
|
+
# GUIDE GENERATOR
|
|
537
|
+
# ============================================================================
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
class GuideGenerator:
|
|
541
|
+
"""Generate markdown guides from workflow data"""
|
|
542
|
+
|
|
543
|
+
def generate_guide_markdown(self, guide: HowToGuide) -> str:
|
|
544
|
+
"""
|
|
545
|
+
Generate complete markdown guide.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
guide: HowToGuide object with all data
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
Complete markdown string
|
|
552
|
+
"""
|
|
553
|
+
sections = []
|
|
554
|
+
|
|
555
|
+
# Header
|
|
556
|
+
sections.append(self._create_header(guide))
|
|
557
|
+
|
|
558
|
+
# Overview
|
|
559
|
+
sections.append(self._create_overview(guide))
|
|
560
|
+
|
|
561
|
+
# Prerequisites
|
|
562
|
+
if guide.prerequisites or guide.required_imports or guide.required_fixtures:
|
|
563
|
+
sections.append(self._create_prerequisites(guide))
|
|
564
|
+
|
|
565
|
+
# Step-by-step guide
|
|
566
|
+
sections.append(self._create_steps_section(guide.steps))
|
|
567
|
+
|
|
568
|
+
# Complete example
|
|
569
|
+
sections.append(self._create_complete_example(guide))
|
|
570
|
+
|
|
571
|
+
# Troubleshooting (if available)
|
|
572
|
+
if guide.common_pitfalls or guide.troubleshooting:
|
|
573
|
+
sections.append(self._create_troubleshooting(guide))
|
|
574
|
+
|
|
575
|
+
# Next steps and related guides
|
|
576
|
+
sections.append(self._create_next_steps(guide))
|
|
577
|
+
|
|
578
|
+
# Footer
|
|
579
|
+
sections.append(self._create_footer(guide))
|
|
580
|
+
|
|
581
|
+
return "\n\n".join(sections)
|
|
582
|
+
|
|
583
|
+
def _create_header(self, guide: HowToGuide) -> str:
|
|
584
|
+
"""Create guide header with metadata"""
|
|
585
|
+
lines = [f"# How To: {guide.title}"]
|
|
586
|
+
lines.append("")
|
|
587
|
+
lines.append(f"**Difficulty**: {guide.complexity_level.title()}")
|
|
588
|
+
lines.append(f"**Estimated Time**: {guide.estimated_time}")
|
|
589
|
+
|
|
590
|
+
if guide.tags:
|
|
591
|
+
lines.append(f"**Tags**: {', '.join(guide.tags)}")
|
|
592
|
+
|
|
593
|
+
return "\n".join(lines)
|
|
594
|
+
|
|
595
|
+
def _create_overview(self, guide: HowToGuide) -> str:
|
|
596
|
+
"""Create overview section"""
|
|
597
|
+
return f"## Overview\n\n{guide.overview}"
|
|
598
|
+
|
|
599
|
+
def _create_prerequisites(self, guide: HowToGuide) -> str:
|
|
600
|
+
"""Create prerequisites section"""
|
|
601
|
+
lines = ["## Prerequisites"]
|
|
602
|
+
lines.append("")
|
|
603
|
+
|
|
604
|
+
# Checklist format
|
|
605
|
+
if guide.prerequisites:
|
|
606
|
+
for prereq in guide.prerequisites:
|
|
607
|
+
lines.append(f"- [ ] {prereq}")
|
|
608
|
+
lines.append("")
|
|
609
|
+
|
|
610
|
+
# Required imports
|
|
611
|
+
if guide.required_imports:
|
|
612
|
+
lines.append("**Required Modules:**")
|
|
613
|
+
for imp in guide.required_imports:
|
|
614
|
+
lines.append(f"- `{imp}`")
|
|
615
|
+
lines.append("")
|
|
616
|
+
|
|
617
|
+
# Required fixtures
|
|
618
|
+
if guide.required_fixtures:
|
|
619
|
+
lines.append("**Required Fixtures:**")
|
|
620
|
+
for fixture in guide.required_fixtures:
|
|
621
|
+
lines.append(f"- `{fixture}` fixture")
|
|
622
|
+
lines.append("")
|
|
623
|
+
|
|
624
|
+
# Setup code if available
|
|
625
|
+
if guide.workflows and guide.workflows[0].get("setup_code"):
|
|
626
|
+
setup_code = guide.workflows[0]["setup_code"]
|
|
627
|
+
lines.append("**Setup Required:**")
|
|
628
|
+
lines.append("```python")
|
|
629
|
+
lines.append(setup_code)
|
|
630
|
+
lines.append("```")
|
|
631
|
+
|
|
632
|
+
return "\n".join(lines)
|
|
633
|
+
|
|
634
|
+
def _create_steps_section(self, steps: list[WorkflowStep]) -> str:
|
|
635
|
+
"""Create step-by-step guide section"""
|
|
636
|
+
lines = ["## Step-by-Step Guide"]
|
|
637
|
+
lines.append("")
|
|
638
|
+
|
|
639
|
+
for step in steps:
|
|
640
|
+
lines.append(f"### Step {step.step_number}: {step.description}")
|
|
641
|
+
lines.append("")
|
|
642
|
+
|
|
643
|
+
# Explanation if available
|
|
644
|
+
if step.explanation:
|
|
645
|
+
lines.append(f"**What you're doing:** {step.explanation}")
|
|
646
|
+
lines.append("")
|
|
647
|
+
|
|
648
|
+
# Code
|
|
649
|
+
lines.append("```python")
|
|
650
|
+
lines.append(step.code)
|
|
651
|
+
lines.append("```")
|
|
652
|
+
lines.append("")
|
|
653
|
+
|
|
654
|
+
# Expected result
|
|
655
|
+
if step.expected_result:
|
|
656
|
+
lines.append(f"**Expected Result:** {step.expected_result}")
|
|
657
|
+
lines.append("")
|
|
658
|
+
|
|
659
|
+
# Verification checkpoint
|
|
660
|
+
if step.verification:
|
|
661
|
+
lines.append("**Verification:**")
|
|
662
|
+
lines.append("```python")
|
|
663
|
+
lines.append(step.verification)
|
|
664
|
+
lines.append("```")
|
|
665
|
+
lines.append("")
|
|
666
|
+
|
|
667
|
+
# Common pitfall warning
|
|
668
|
+
if step.common_pitfall:
|
|
669
|
+
lines.append(f"⚠️ **Common Pitfall:** {step.common_pitfall}")
|
|
670
|
+
lines.append("")
|
|
671
|
+
|
|
672
|
+
return "\n".join(lines)
|
|
673
|
+
|
|
674
|
+
def _create_complete_example(self, guide: HowToGuide) -> str:
|
|
675
|
+
"""Create complete working example"""
|
|
676
|
+
lines = ["## Complete Example"]
|
|
677
|
+
lines.append("")
|
|
678
|
+
lines.append("```python")
|
|
679
|
+
|
|
680
|
+
# If we have workflows, use the first one's code
|
|
681
|
+
if guide.workflows:
|
|
682
|
+
workflow = guide.workflows[0]
|
|
683
|
+
|
|
684
|
+
# Add setup code if present
|
|
685
|
+
if workflow.get("setup_code"):
|
|
686
|
+
lines.append("# Setup")
|
|
687
|
+
lines.append(workflow["setup_code"])
|
|
688
|
+
lines.append("")
|
|
689
|
+
|
|
690
|
+
# Add main workflow code
|
|
691
|
+
lines.append("# Workflow")
|
|
692
|
+
lines.append(workflow.get("code", ""))
|
|
693
|
+
else:
|
|
694
|
+
# Combine all steps
|
|
695
|
+
for step in guide.steps:
|
|
696
|
+
lines.append(f"# Step {step.step_number}: {step.description}")
|
|
697
|
+
lines.append(step.code)
|
|
698
|
+
if step.verification:
|
|
699
|
+
lines.append(step.verification)
|
|
700
|
+
lines.append("")
|
|
701
|
+
|
|
702
|
+
lines.append("```")
|
|
703
|
+
return "\n".join(lines)
|
|
704
|
+
|
|
705
|
+
def _create_troubleshooting(self, guide: HowToGuide) -> str:
|
|
706
|
+
"""Create troubleshooting section"""
|
|
707
|
+
lines = ["## Troubleshooting"]
|
|
708
|
+
lines.append("")
|
|
709
|
+
|
|
710
|
+
# Common pitfalls
|
|
711
|
+
if guide.common_pitfalls:
|
|
712
|
+
lines.append("### Common Issues")
|
|
713
|
+
lines.append("")
|
|
714
|
+
for i, pitfall in enumerate(guide.common_pitfalls, 1):
|
|
715
|
+
lines.append(f"{i}. {pitfall}")
|
|
716
|
+
lines.append("")
|
|
717
|
+
|
|
718
|
+
# Specific troubleshooting
|
|
719
|
+
if guide.troubleshooting:
|
|
720
|
+
for problem, solution in guide.troubleshooting.items():
|
|
721
|
+
lines.append(f"### Problem: {problem}")
|
|
722
|
+
lines.append("")
|
|
723
|
+
lines.append(f"**Solution:** {solution}")
|
|
724
|
+
lines.append("")
|
|
725
|
+
|
|
726
|
+
return "\n".join(lines)
|
|
727
|
+
|
|
728
|
+
def _create_next_steps(self, guide: HowToGuide) -> str:
|
|
729
|
+
"""Create next steps and related guides"""
|
|
730
|
+
lines = ["## Next Steps"]
|
|
731
|
+
lines.append("")
|
|
732
|
+
|
|
733
|
+
# Variations if available
|
|
734
|
+
if guide.variations:
|
|
735
|
+
lines.append("**Try these variations:**")
|
|
736
|
+
for variation in guide.variations:
|
|
737
|
+
lines.append(f"- {variation}")
|
|
738
|
+
lines.append("")
|
|
739
|
+
|
|
740
|
+
# Related guides
|
|
741
|
+
if guide.related_guides:
|
|
742
|
+
lines.append("## Related Guides")
|
|
743
|
+
lines.append("")
|
|
744
|
+
for related in guide.related_guides:
|
|
745
|
+
lines.append(f"- [{related}]")
|
|
746
|
+
lines.append("")
|
|
747
|
+
|
|
748
|
+
return "\n".join(lines)
|
|
749
|
+
|
|
750
|
+
def _create_footer(self, guide: HowToGuide) -> str:
|
|
751
|
+
"""Create guide footer with metadata"""
|
|
752
|
+
source_info = []
|
|
753
|
+
if guide.source_files:
|
|
754
|
+
source_info.append(f"Source: {', '.join(guide.source_files)}")
|
|
755
|
+
source_info.append(f"Complexity: {guide.complexity_level.title()}")
|
|
756
|
+
source_info.append(f"Last updated: {datetime.now().strftime('%Y-%m-%d')}")
|
|
757
|
+
|
|
758
|
+
return f"---\n\n*{' | '.join(source_info)}*"
|
|
759
|
+
|
|
760
|
+
def generate_index(self, guides: list[HowToGuide]) -> str:
|
|
761
|
+
"""
|
|
762
|
+
Generate index/TOC markdown.
|
|
763
|
+
|
|
764
|
+
Args:
|
|
765
|
+
guides: List of all guides
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
Index markdown string
|
|
769
|
+
"""
|
|
770
|
+
lines = ["# How-To Guides Index"]
|
|
771
|
+
lines.append("")
|
|
772
|
+
lines.append(f"**Total Guides**: {len(guides)}")
|
|
773
|
+
lines.append(f"**Last Updated**: {datetime.now().strftime('%Y-%m-%d')}")
|
|
774
|
+
lines.append("")
|
|
775
|
+
|
|
776
|
+
# Group by use case
|
|
777
|
+
by_use_case = defaultdict(list)
|
|
778
|
+
for guide in guides:
|
|
779
|
+
use_case = guide.use_case or "Other"
|
|
780
|
+
by_use_case[use_case].append(guide)
|
|
781
|
+
|
|
782
|
+
lines.append("## By Use Case")
|
|
783
|
+
lines.append("")
|
|
784
|
+
|
|
785
|
+
for use_case in sorted(by_use_case.keys()):
|
|
786
|
+
case_guides = by_use_case[use_case]
|
|
787
|
+
lines.append(f"### {use_case} ({len(case_guides)} guides)")
|
|
788
|
+
for guide in sorted(case_guides, key=lambda g: g.complexity_level):
|
|
789
|
+
# Create filename from guide title
|
|
790
|
+
filename = guide.title.lower().replace(" ", "-").replace(":", "")
|
|
791
|
+
lines.append(
|
|
792
|
+
f"- [How To: {guide.title}]({use_case.lower()}/{filename}.md) - {guide.complexity_level.title()}"
|
|
793
|
+
)
|
|
794
|
+
lines.append("")
|
|
795
|
+
|
|
796
|
+
# Group by difficulty
|
|
797
|
+
by_complexity = defaultdict(list)
|
|
798
|
+
for guide in guides:
|
|
799
|
+
by_complexity[guide.complexity_level].append(guide)
|
|
800
|
+
|
|
801
|
+
lines.append("## By Difficulty Level")
|
|
802
|
+
lines.append("")
|
|
803
|
+
|
|
804
|
+
for level in ["beginner", "intermediate", "advanced"]:
|
|
805
|
+
if level in by_complexity:
|
|
806
|
+
level_guides = by_complexity[level]
|
|
807
|
+
lines.append(f"### {level.title()} ({len(level_guides)} guides)")
|
|
808
|
+
for guide in sorted(level_guides, key=lambda g: g.title):
|
|
809
|
+
lines.append(f"- {guide.title}")
|
|
810
|
+
lines.append("")
|
|
811
|
+
|
|
812
|
+
return "\n".join(lines)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
# ============================================================================
|
|
816
|
+
# HOW-TO GUIDE BUILDER (Main Orchestrator)
|
|
817
|
+
# ============================================================================
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
class HowToGuideBuilder:
|
|
821
|
+
"""Main orchestrator for building how-to guides from workflow examples"""
|
|
822
|
+
|
|
823
|
+
def __init__(self, enhance_with_ai: bool = True):
|
|
824
|
+
"""
|
|
825
|
+
Initialize guide builder.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
enhance_with_ai: Enable AI enhancement (requires C3.6 AI analysis in workflows)
|
|
829
|
+
"""
|
|
830
|
+
self.enhance_with_ai = enhance_with_ai
|
|
831
|
+
self.analyzer = WorkflowAnalyzer()
|
|
832
|
+
self.grouper = WorkflowGrouper()
|
|
833
|
+
self.generator = GuideGenerator()
|
|
834
|
+
|
|
835
|
+
def build_guides_from_examples(
|
|
836
|
+
self,
|
|
837
|
+
examples: list[dict],
|
|
838
|
+
grouping_strategy: str = "ai-tutorial-group",
|
|
839
|
+
output_dir: Path | None = None,
|
|
840
|
+
enhance_with_ai: bool = True,
|
|
841
|
+
ai_mode: str = "auto",
|
|
842
|
+
) -> GuideCollection:
|
|
843
|
+
"""
|
|
844
|
+
Main entry point - build guides from workflow examples.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
examples: List of TestExample dicts from C3.2
|
|
848
|
+
grouping_strategy: How to group workflows ("ai-tutorial-group", "file-path", etc.)
|
|
849
|
+
output_dir: Optional directory to save markdown files
|
|
850
|
+
enhance_with_ai: Enable comprehensive AI enhancement (default: True)
|
|
851
|
+
ai_mode: AI enhancement mode - "auto", "api", "local", or "none"
|
|
852
|
+
|
|
853
|
+
Returns:
|
|
854
|
+
GuideCollection with all generated guides
|
|
855
|
+
"""
|
|
856
|
+
logger.info(f"Building how-to guides from {len(examples)} examples...")
|
|
857
|
+
|
|
858
|
+
# Initialize AI enhancer if requested
|
|
859
|
+
enhancer = None
|
|
860
|
+
if enhance_with_ai and ai_mode != "none":
|
|
861
|
+
try:
|
|
862
|
+
from .guide_enhancer import GuideEnhancer
|
|
863
|
+
|
|
864
|
+
enhancer = GuideEnhancer(mode=ai_mode)
|
|
865
|
+
logger.info(f"✨ AI enhancement enabled (mode: {enhancer.mode})")
|
|
866
|
+
except Exception as e:
|
|
867
|
+
logger.warning(f"⚠️ AI enhancement unavailable: {e}")
|
|
868
|
+
logger.info("📝 Falling back to basic guide generation")
|
|
869
|
+
|
|
870
|
+
# Filter to workflow examples only
|
|
871
|
+
workflows = self._extract_workflow_examples(examples)
|
|
872
|
+
logger.info(f"Found {len(workflows)} workflow examples")
|
|
873
|
+
|
|
874
|
+
if not workflows:
|
|
875
|
+
logger.warning("No workflow examples found!")
|
|
876
|
+
return GuideCollection(
|
|
877
|
+
total_guides=0,
|
|
878
|
+
guides_by_complexity={},
|
|
879
|
+
guides_by_use_case={},
|
|
880
|
+
guides=[],
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
# Group workflows
|
|
884
|
+
grouped_workflows = self.grouper.group_workflows(workflows, grouping_strategy)
|
|
885
|
+
logger.info(f"Grouped into {len(grouped_workflows)} guide categories")
|
|
886
|
+
|
|
887
|
+
# Build guides
|
|
888
|
+
guides = []
|
|
889
|
+
for title, workflow_group in grouped_workflows.items():
|
|
890
|
+
guide = self._create_guide(title, workflow_group, enhancer)
|
|
891
|
+
guides.append(guide)
|
|
892
|
+
|
|
893
|
+
# Create collection
|
|
894
|
+
collection = self._create_collection(guides)
|
|
895
|
+
|
|
896
|
+
# Save to files if output directory provided
|
|
897
|
+
if output_dir:
|
|
898
|
+
self._save_guides_to_files(collection, output_dir)
|
|
899
|
+
|
|
900
|
+
logger.info(f"✅ Generated {len(guides)} how-to guides")
|
|
901
|
+
return collection
|
|
902
|
+
|
|
903
|
+
def _extract_workflow_examples(self, examples: list[dict]) -> list[dict]:
|
|
904
|
+
"""Filter to workflow category only"""
|
|
905
|
+
return [ex for ex in examples if ex.get("category") == "workflow"]
|
|
906
|
+
|
|
907
|
+
def _create_guide(self, title: str, workflows: list[dict], enhancer=None) -> HowToGuide:
|
|
908
|
+
"""
|
|
909
|
+
Generate single guide from workflow(s).
|
|
910
|
+
|
|
911
|
+
Args:
|
|
912
|
+
title: Guide title
|
|
913
|
+
workflows: List of related workflow examples
|
|
914
|
+
enhancer: Optional GuideEnhancer instance for AI enhancement
|
|
915
|
+
|
|
916
|
+
Returns:
|
|
917
|
+
Complete HowToGuide object
|
|
918
|
+
"""
|
|
919
|
+
# Use first workflow as primary
|
|
920
|
+
primary_workflow = workflows[0]
|
|
921
|
+
|
|
922
|
+
# Analyze workflow to extract steps
|
|
923
|
+
steps, metadata = self.analyzer.analyze_workflow(primary_workflow)
|
|
924
|
+
|
|
925
|
+
# Generate guide ID
|
|
926
|
+
guide_id = hashlib.md5(title.encode()).hexdigest()[:12]
|
|
927
|
+
|
|
928
|
+
# Extract use case from AI analysis or title
|
|
929
|
+
use_case = title
|
|
930
|
+
if primary_workflow.get("ai_analysis"):
|
|
931
|
+
use_case = primary_workflow["ai_analysis"].get("tutorial_group", title)
|
|
932
|
+
|
|
933
|
+
# Determine overview
|
|
934
|
+
overview = self._generate_overview(primary_workflow, workflows)
|
|
935
|
+
|
|
936
|
+
# Extract tags
|
|
937
|
+
tags = primary_workflow.get("tags", [])
|
|
938
|
+
|
|
939
|
+
# Extract source files
|
|
940
|
+
source_files = [w.get("file_path", "") for w in workflows]
|
|
941
|
+
source_files = [
|
|
942
|
+
f"{Path(f).name}:{w.get('line_start', 0)}"
|
|
943
|
+
for f, w in zip(source_files, workflows, strict=False)
|
|
944
|
+
]
|
|
945
|
+
|
|
946
|
+
# Create guide
|
|
947
|
+
guide = HowToGuide(
|
|
948
|
+
guide_id=guide_id,
|
|
949
|
+
title=title,
|
|
950
|
+
overview=overview,
|
|
951
|
+
complexity_level=metadata.get("complexity_level", "intermediate"),
|
|
952
|
+
prerequisites=metadata.get("prerequisites", []),
|
|
953
|
+
required_imports=metadata.get("required_imports", []),
|
|
954
|
+
required_fixtures=metadata.get("required_fixtures", []),
|
|
955
|
+
workflows=workflows,
|
|
956
|
+
steps=steps,
|
|
957
|
+
use_case=use_case,
|
|
958
|
+
tags=tags,
|
|
959
|
+
estimated_time=metadata.get("estimated_time", "10 minutes"),
|
|
960
|
+
source_files=source_files,
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
# Add AI enhancements if enhancer is available
|
|
964
|
+
if enhancer:
|
|
965
|
+
self._enhance_guide_with_ai(guide, primary_workflow.get("ai_analysis", {}), enhancer)
|
|
966
|
+
elif self.enhance_with_ai and primary_workflow.get("ai_analysis"):
|
|
967
|
+
# Fallback to old enhancement method (basic)
|
|
968
|
+
self._enhance_guide_with_ai_basic(guide, primary_workflow["ai_analysis"])
|
|
969
|
+
|
|
970
|
+
return guide
|
|
971
|
+
|
|
972
|
+
def _generate_overview(self, primary_workflow: dict, _all_workflows: list[dict]) -> str:
|
|
973
|
+
"""Generate guide overview"""
|
|
974
|
+
# Try to get explanation from AI analysis
|
|
975
|
+
if primary_workflow.get("ai_analysis"):
|
|
976
|
+
explanation = primary_workflow["ai_analysis"].get("explanation")
|
|
977
|
+
if explanation:
|
|
978
|
+
return explanation
|
|
979
|
+
|
|
980
|
+
# Fallback to description
|
|
981
|
+
description = primary_workflow.get("description", "")
|
|
982
|
+
if description:
|
|
983
|
+
return description
|
|
984
|
+
|
|
985
|
+
# Final fallback
|
|
986
|
+
return f"Learn how to use {primary_workflow.get('test_name', 'this feature')} in your code."
|
|
987
|
+
|
|
988
|
+
def _enhance_guide_with_ai(self, guide: HowToGuide, _ai_analysis: dict, enhancer):
|
|
989
|
+
"""
|
|
990
|
+
Comprehensively enhance guide with AI using GuideEnhancer.
|
|
991
|
+
|
|
992
|
+
Applies all 5 enhancements:
|
|
993
|
+
1. Step descriptions - Natural language explanations 2. Troubleshooting - Diagnostic flows + solutions
|
|
994
|
+
3. Prerequisites - Why needed + setup
|
|
995
|
+
4. Next steps - Related guides, variations
|
|
996
|
+
5. Use cases - Real-world scenarios
|
|
997
|
+
|
|
998
|
+
Args:
|
|
999
|
+
guide: HowToGuide object to enhance
|
|
1000
|
+
ai_analysis: AI analysis data from C3.6 (for context)
|
|
1001
|
+
enhancer: GuideEnhancer instance
|
|
1002
|
+
"""
|
|
1003
|
+
# Prepare guide data for enhancer
|
|
1004
|
+
guide_data = {
|
|
1005
|
+
"title": guide.title,
|
|
1006
|
+
"steps": [{"description": step.description, "code": step.code} for step in guide.steps],
|
|
1007
|
+
"language": "python", # TODO: Detect from code
|
|
1008
|
+
"prerequisites": guide.prerequisites,
|
|
1009
|
+
"description": guide.overview,
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
# Call enhancer to get all 5 enhancements
|
|
1013
|
+
enhanced_data = enhancer.enhance_guide(guide_data)
|
|
1014
|
+
|
|
1015
|
+
# Apply step enhancements
|
|
1016
|
+
if "step_enhancements" in enhanced_data:
|
|
1017
|
+
for enhancement in enhanced_data["step_enhancements"]:
|
|
1018
|
+
idx = enhancement.step_index
|
|
1019
|
+
if 0 <= idx < len(guide.steps):
|
|
1020
|
+
guide.steps[idx].explanation = enhancement.explanation
|
|
1021
|
+
guide.steps[idx].common_variations = enhancement.variations
|
|
1022
|
+
|
|
1023
|
+
# Apply detailed prerequisites
|
|
1024
|
+
if "prerequisites_detailed" in enhanced_data:
|
|
1025
|
+
guide.prerequisites_detailed = enhanced_data["prerequisites_detailed"]
|
|
1026
|
+
|
|
1027
|
+
# Apply troubleshooting
|
|
1028
|
+
if "troubleshooting_detailed" in enhanced_data:
|
|
1029
|
+
guide.troubleshooting_detailed = enhanced_data["troubleshooting_detailed"]
|
|
1030
|
+
|
|
1031
|
+
# Apply next steps
|
|
1032
|
+
if "next_steps_detailed" in enhanced_data:
|
|
1033
|
+
guide.next_steps_detailed = enhanced_data["next_steps_detailed"]
|
|
1034
|
+
|
|
1035
|
+
# Apply use cases
|
|
1036
|
+
if "use_cases" in enhanced_data:
|
|
1037
|
+
guide.use_cases = enhanced_data["use_cases"]
|
|
1038
|
+
|
|
1039
|
+
logger.info(f"✨ Enhanced guide '{guide.title}' with comprehensive AI improvements")
|
|
1040
|
+
|
|
1041
|
+
def _enhance_guide_with_ai_basic(self, guide: HowToGuide, ai_analysis: dict):
|
|
1042
|
+
"""
|
|
1043
|
+
Basic enhancement using pre-computed AI analysis from C3.6.
|
|
1044
|
+
|
|
1045
|
+
This is a fallback when GuideEnhancer is not available.
|
|
1046
|
+
|
|
1047
|
+
Args:
|
|
1048
|
+
guide: HowToGuide object to enhance
|
|
1049
|
+
ai_analysis: AI analysis data from C3.6
|
|
1050
|
+
"""
|
|
1051
|
+
# Add best practices as variations
|
|
1052
|
+
best_practices = ai_analysis.get("best_practices", [])
|
|
1053
|
+
guide.variations = best_practices
|
|
1054
|
+
|
|
1055
|
+
# Add common mistakes as pitfalls
|
|
1056
|
+
common_mistakes = ai_analysis.get("common_mistakes", [])
|
|
1057
|
+
guide.common_pitfalls = common_mistakes
|
|
1058
|
+
|
|
1059
|
+
# Add related examples as related guides
|
|
1060
|
+
related_examples = ai_analysis.get("related_examples", [])
|
|
1061
|
+
guide.related_guides = [f"How To: {ex}" for ex in related_examples]
|
|
1062
|
+
|
|
1063
|
+
# Enhance step explanations
|
|
1064
|
+
for step in guide.steps:
|
|
1065
|
+
# Add explanation to steps based on best practices
|
|
1066
|
+
if best_practices and step.step_number <= len(best_practices):
|
|
1067
|
+
step.explanation = best_practices[step.step_number - 1]
|
|
1068
|
+
|
|
1069
|
+
def _create_collection(self, guides: list[HowToGuide]) -> GuideCollection:
|
|
1070
|
+
"""Create GuideCollection from guides"""
|
|
1071
|
+
# Count by complexity
|
|
1072
|
+
by_complexity = defaultdict(int)
|
|
1073
|
+
for guide in guides:
|
|
1074
|
+
by_complexity[guide.complexity_level] += 1
|
|
1075
|
+
|
|
1076
|
+
# Group by use case
|
|
1077
|
+
by_use_case = defaultdict(list)
|
|
1078
|
+
for guide in guides:
|
|
1079
|
+
use_case = guide.use_case or "Other"
|
|
1080
|
+
by_use_case[use_case].append(guide)
|
|
1081
|
+
|
|
1082
|
+
return GuideCollection(
|
|
1083
|
+
total_guides=len(guides),
|
|
1084
|
+
guides_by_complexity=dict(by_complexity),
|
|
1085
|
+
guides_by_use_case=dict(by_use_case),
|
|
1086
|
+
guides=guides,
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
def _save_guides_to_files(self, collection: GuideCollection, output_dir: Path):
|
|
1090
|
+
"""Save guides to markdown files"""
|
|
1091
|
+
output_dir = Path(output_dir)
|
|
1092
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
1093
|
+
|
|
1094
|
+
logger.info(f"Saving guides to {output_dir}...")
|
|
1095
|
+
|
|
1096
|
+
# Save individual guides
|
|
1097
|
+
for use_case, guides in collection.guides_by_use_case.items():
|
|
1098
|
+
# Create use case directory
|
|
1099
|
+
use_case_dir = output_dir / use_case.lower().replace(" ", "-")
|
|
1100
|
+
use_case_dir.mkdir(parents=True, exist_ok=True)
|
|
1101
|
+
|
|
1102
|
+
for guide in guides:
|
|
1103
|
+
# Generate filename from title
|
|
1104
|
+
filename = guide.title.lower().replace(" ", "-").replace(":", "") + ".md"
|
|
1105
|
+
file_path = use_case_dir / filename
|
|
1106
|
+
|
|
1107
|
+
# Generate and save markdown
|
|
1108
|
+
markdown = self.generator.generate_guide_markdown(guide)
|
|
1109
|
+
file_path.write_text(markdown, encoding="utf-8")
|
|
1110
|
+
|
|
1111
|
+
# Save index
|
|
1112
|
+
index_markdown = self.generator.generate_index(collection.guides)
|
|
1113
|
+
(output_dir / "index.md").write_text(index_markdown, encoding="utf-8")
|
|
1114
|
+
|
|
1115
|
+
logger.info(f"✅ Saved {collection.total_guides} guides + index to {output_dir}")
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
# ============================================================================
|
|
1119
|
+
# CLI INTERFACE
|
|
1120
|
+
# ============================================================================
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def main():
|
|
1124
|
+
"""CLI entry point for how-to guide builder"""
|
|
1125
|
+
import argparse
|
|
1126
|
+
import sys
|
|
1127
|
+
|
|
1128
|
+
parser = argparse.ArgumentParser(
|
|
1129
|
+
description="Build how-to guides from workflow test examples (C3.3)",
|
|
1130
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1131
|
+
epilog="""
|
|
1132
|
+
Examples:
|
|
1133
|
+
# From test examples JSON (C3.2 output)
|
|
1134
|
+
skill-seekers build-how-to-guides --input test_examples.json
|
|
1135
|
+
|
|
1136
|
+
# From directory (extracts workflows)
|
|
1137
|
+
skill-seekers build-how-to-guides tests/
|
|
1138
|
+
|
|
1139
|
+
# Custom grouping strategy
|
|
1140
|
+
skill-seekers build-how-to-guides tests/ --group-by file-path
|
|
1141
|
+
|
|
1142
|
+
# Custom output directory
|
|
1143
|
+
skill-seekers build-how-to-guides tests/ --output tutorials/
|
|
1144
|
+
|
|
1145
|
+
# Without AI enhancement
|
|
1146
|
+
skill-seekers build-how-to-guides tests/ --no-ai
|
|
1147
|
+
|
|
1148
|
+
Grouping Strategies:
|
|
1149
|
+
- ai-tutorial-group: Use AI-generated tutorial groups (default, best)
|
|
1150
|
+
- file-path: Group by source test file
|
|
1151
|
+
- test-name: Group by test name patterns
|
|
1152
|
+
- complexity: Group by difficulty level
|
|
1153
|
+
""",
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
parser.add_argument(
|
|
1157
|
+
"input",
|
|
1158
|
+
nargs="?",
|
|
1159
|
+
help="Input: directory with test files OR test_examples.json file",
|
|
1160
|
+
)
|
|
1161
|
+
|
|
1162
|
+
parser.add_argument(
|
|
1163
|
+
"--input",
|
|
1164
|
+
dest="input_file",
|
|
1165
|
+
help="Input JSON file with test examples (from C3.2)",
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
parser.add_argument(
|
|
1169
|
+
"--output",
|
|
1170
|
+
default="output/codebase/tutorials",
|
|
1171
|
+
help="Output directory for generated guides (default: output/codebase/tutorials)",
|
|
1172
|
+
)
|
|
1173
|
+
|
|
1174
|
+
parser.add_argument(
|
|
1175
|
+
"--group-by",
|
|
1176
|
+
choices=["ai-tutorial-group", "file-path", "test-name", "complexity"],
|
|
1177
|
+
default="ai-tutorial-group",
|
|
1178
|
+
help="Grouping strategy (default: ai-tutorial-group)",
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
parser.add_argument("--no-ai", action="store_true", help="Disable AI enhancement")
|
|
1182
|
+
|
|
1183
|
+
parser.add_argument(
|
|
1184
|
+
"--json-output",
|
|
1185
|
+
action="store_true",
|
|
1186
|
+
help="Output JSON summary instead of markdown files",
|
|
1187
|
+
)
|
|
1188
|
+
|
|
1189
|
+
args = parser.parse_args()
|
|
1190
|
+
|
|
1191
|
+
# Determine input source
|
|
1192
|
+
input_path = args.input or args.input_file
|
|
1193
|
+
|
|
1194
|
+
if not input_path:
|
|
1195
|
+
parser.print_help()
|
|
1196
|
+
print("\n❌ Error: No input provided")
|
|
1197
|
+
print(" Provide either a directory or --input JSON file")
|
|
1198
|
+
sys.exit(1)
|
|
1199
|
+
|
|
1200
|
+
input_path = Path(input_path)
|
|
1201
|
+
|
|
1202
|
+
# Load examples
|
|
1203
|
+
examples = []
|
|
1204
|
+
|
|
1205
|
+
if input_path.is_file() and input_path.suffix == ".json":
|
|
1206
|
+
# Load from JSON file
|
|
1207
|
+
logger.info(f"Loading examples from {input_path}...")
|
|
1208
|
+
with open(input_path) as f:
|
|
1209
|
+
data = json.load(f)
|
|
1210
|
+
if isinstance(data, dict) and "examples" in data:
|
|
1211
|
+
examples = data["examples"]
|
|
1212
|
+
elif isinstance(data, list):
|
|
1213
|
+
examples = data
|
|
1214
|
+
else:
|
|
1215
|
+
print(f"❌ Error: Invalid JSON format in {input_path}")
|
|
1216
|
+
sys.exit(1)
|
|
1217
|
+
|
|
1218
|
+
elif input_path.is_dir():
|
|
1219
|
+
# Extract from directory using test example extractor
|
|
1220
|
+
print("⚠️ Directory input requires test example extractor")
|
|
1221
|
+
print(" Please use test_examples.json output from C3.2")
|
|
1222
|
+
print(f" Or run: skill-seekers extract-test-examples {input_path} --json > examples.json")
|
|
1223
|
+
sys.exit(1)
|
|
1224
|
+
|
|
1225
|
+
else:
|
|
1226
|
+
print(f"❌ Error: Input path not found: {input_path}")
|
|
1227
|
+
sys.exit(1)
|
|
1228
|
+
|
|
1229
|
+
# Build guides
|
|
1230
|
+
builder = HowToGuideBuilder(enhance_with_ai=not args.no_ai)
|
|
1231
|
+
output_dir = Path(args.output) if not args.json_output else None
|
|
1232
|
+
|
|
1233
|
+
collection = builder.build_guides_from_examples(
|
|
1234
|
+
examples, grouping_strategy=args.group_by, output_dir=output_dir
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
# Output results
|
|
1238
|
+
if args.json_output:
|
|
1239
|
+
# JSON output
|
|
1240
|
+
print(json.dumps(collection.to_dict(), indent=2))
|
|
1241
|
+
else:
|
|
1242
|
+
# Summary
|
|
1243
|
+
print()
|
|
1244
|
+
print("=" * 60)
|
|
1245
|
+
print("HOW-TO GUIDES GENERATED")
|
|
1246
|
+
print("=" * 60)
|
|
1247
|
+
print()
|
|
1248
|
+
print(f"Total Guides: {collection.total_guides}")
|
|
1249
|
+
print()
|
|
1250
|
+
print("By Complexity:")
|
|
1251
|
+
for level, count in collection.guides_by_complexity.items():
|
|
1252
|
+
print(f" - {level.title()}: {count} guides")
|
|
1253
|
+
print()
|
|
1254
|
+
print("By Use Case:")
|
|
1255
|
+
for use_case, guides in collection.guides_by_use_case.items():
|
|
1256
|
+
print(f" - {use_case}: {len(guides)} guides")
|
|
1257
|
+
print()
|
|
1258
|
+
if output_dir:
|
|
1259
|
+
print(f"📁 Output directory: {output_dir}")
|
|
1260
|
+
print(f"📄 Index file: {output_dir}/index.md")
|
|
1261
|
+
print()
|
|
1262
|
+
|
|
1263
|
+
sys.exit(0)
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
if __name__ == "__main__":
|
|
1267
|
+
main()
|