Kode-Animator's picture
Initial commit - ReFinEd AI Backend v1.0
feae69f
Raw History Blame Contribute Delete
4.79 kB
"""
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