""" Lessons Loader Module - Load and search lesson content Lessons are stored as JSON in the data/ directory. """ import json import os from typing import Dict, List, Optional, Any from pathlib import Path # Cache for loaded lessons _lessons_cache: Optional[List[Dict[str, Any]]] = None def _load_lessons() -> List[Dict[str, Any]]: """Load lessons from JSON file.""" global _lessons_cache if _lessons_cache is not None: return _lessons_cache data_path = Path(__file__).parent.parent.parent / "data" / "lessons.json" if not data_path.exists(): # Return empty list if no lessons file yet return [] with open(data_path, "r", encoding="utf-8") as f: _lessons_cache = json.load(f) return _lessons_cache def get_all_lessons() -> List[Dict[str, Any]]: """Get all lessons (metadata only).""" lessons = _load_lessons() # Return metadata only (no full content) return [ { "id": l.get("id"), "slug": l.get("slug"), "title": l.get("title"), "description": l.get("description"), "difficulty": l.get("difficulty"), "isPro": l.get("isPro", False) } for l in lessons ] def get_lesson_by_id(lesson_id: str) -> Optional[Dict[str, Any]]: """Get a lesson by its numeric ID.""" lessons = _load_lessons() for lesson in lessons: if str(lesson.get("id")) == str(lesson_id): return lesson return None def get_lesson_by_slug(slug: str) -> Optional[Dict[str, Any]]: """Get a lesson by its slug.""" lessons = _load_lessons() for lesson in lessons: if lesson.get("slug") == slug: return lesson return None def search_lessons(query: str) -> List[Dict[str, Any]]: """Search lessons by title, description, or content using keyword scoring.""" lessons = _load_lessons() # improved tokenization stop_words = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "is", "are", "was", "were", "what", "how", "why", "who", "does", "do", "can"} query_tokens = {word.lower() for word in query.split() if word.lower() not in stop_words and len(word) > 2} if not query_tokens: return [] results = [] for lesson in lessons: score = 0 lesson_title = lesson.get("title", "").lower() lesson_desc = lesson.get("description", "").lower() # Check Title (High weight per token) for token in query_tokens: if token in lesson_title: score += 10 # Check Description for token in query_tokens: if token in lesson_desc: score += 5 # Check Sections for section in lesson.get("sections", []): section_title = section.get("title", "").lower() # FIX: Check 'body' as well as 'content' (legacy vs new) section_text = (section.get("body", "") + " " + section.get("content", "")).lower() for token in query_tokens: if token in section_title: score += 3 if token in section_text: score += 1 # Check Takeaways for takeaway in lesson.get("takeaways", []): takeaway_lower = takeaway.lower() for token in query_tokens: if token in takeaway_lower: score += 2 if score > 0: results.append({ "id": lesson.get("id"), "slug": lesson.get("slug"), "title": lesson.get("title"), "description": lesson.get("description"), "difficulty": lesson.get("difficulty"), "score": score, # Return snippets if needed? For now just metadata is enough context # The prompt will ask for full content if needed or we can return it here? # The 'tools.py' function calls get_lesson_by_id typically? # No, searchLessonContent usually returns snippets or full content? # Let's verify what `tools.py` does. # Just return metadata + validation score for now. "sections": lesson.get("sections", []) if score > 10 else [] # Include content if high match? }) # Sort by relevance score results.sort(key=lambda x: x["score"], reverse=True) # If we have high confidence matches, filter out low noise if results and results[0]["score"] > 20: results = [r for r in results if r["score"] > 5] return results[:5] # Return top 5 results