feat(patterns): tutorial system

This commit is contained in:
2025-08-07 00:41:51 +01:00
parent b4e23cc641
commit 2360727e11
15 changed files with 1386 additions and 45 deletions

View File

@@ -52,33 +52,45 @@ async def load_categories(session: AsyncSession, data_dir: Path) -> dict[str, Ca
async def load_patterns(session: AsyncSession, data_dir: Path) -> dict[str, Pattern]:
"""Load patterns from YAML file."""
patterns_file = data_dir / "patterns" / "patterns.yaml"
if not patterns_file.exists():
print(f"Warning: {patterns_file} not found")
return {}
with open(patterns_file) as f:
data = yaml.safe_load(f)
"""Load patterns from YAML files.
Supports both:
- Legacy single file: patterns/patterns.yaml
- Individual files: patterns/<slug>.yaml (preferred for tutorials)
"""
patterns_dir = data_dir / "patterns"
patterns: dict[str, Pattern] = {}
for item in data.get("patterns", []):
result = await session.execute(select(Pattern).where(Pattern.slug == item["slug"]))
existing = result.scalar_one_or_none()
if existing:
existing.name = item["name"]
existing.description = item.get("description")
existing.when_to_use = item.get("when_to_use")
patterns[item["slug"]] = existing
else:
pattern = Pattern(
name=item["name"],
slug=item["slug"],
description=item.get("description"),
when_to_use=item.get("when_to_use"),
)
session.add(pattern)
# First, try loading individual pattern files (preferred for tutorials)
individual_files = list(patterns_dir.glob("*.yaml"))
# Filter out the legacy patterns.yaml file
individual_files = [f for f in individual_files if f.name != "patterns.yaml"]
if individual_files:
for pattern_file in individual_files:
with open(pattern_file) as f:
item = yaml.safe_load(f)
if not item or "slug" not in item:
print(f" Warning: Skipping {pattern_file.name} - missing slug")
continue
pattern = await _upsert_pattern(session, item)
patterns[item["slug"]] = pattern
print(f" Loaded: {item['name']}")
# Fall back to legacy patterns.yaml if no individual files found
legacy_file = patterns_dir / "patterns.yaml"
if legacy_file.exists():
with open(legacy_file) as f:
data = yaml.safe_load(f)
for item in data.get("patterns", []):
# Skip if already loaded from individual file
if item["slug"] in patterns:
continue
pattern = await _upsert_pattern(session, item)
patterns[item["slug"]] = pattern
await session.flush()
@@ -86,6 +98,41 @@ async def load_patterns(session: AsyncSession, data_dir: Path) -> dict[str, Patt
return patterns
async def _upsert_pattern(session: AsyncSession, item: dict[str, Any]) -> Pattern:
"""Insert or update a single pattern from YAML data."""
result = await session.execute(select(Pattern).where(Pattern.slug == item["slug"]))
existing = result.scalar_one_or_none()
if existing:
pattern = existing
else:
pattern = Pattern(slug=item["slug"])
session.add(pattern)
# Core fields
pattern.name = item["name"]
pattern.description = item.get("description")
pattern.when_to_use = item.get("when_to_use")
# Tutorial content fields
pattern.metaphor = item.get("metaphor")
pattern.core_concept = item.get("core_concept")
pattern.visualization = item.get("visualization")
pattern.code_template = item.get("code_template")
# Structured data fields (JSONB)
pattern.recognition_signals = item.get("recognition_signals")
pattern.common_mistakes = item.get("common_mistakes")
pattern.variations = item.get("variations")
pattern.related_patterns = item.get("related_patterns")
pattern.prerequisite_patterns = item.get("prerequisite_patterns")
# Difficulty level
pattern.difficulty_level = item.get("difficulty_level")
return pattern
async def load_question(
session: AsyncSession,
question_file: Path,