| """Course Builder Service for DocDoe AI. |
| |
| Generates structured course plans from raw user requests. Supports: |
| - School topics (class/board/subject) |
| - Degree semester subjects |
| - Skill courses (machine learning, web dev, etc.) |
| - Playlist-based learning |
| |
| Data priority: |
| 1. Uploaded syllabus / source context |
| 2. Catalog seed data |
| 3. Generic topic scaffolding (no hallucination) |
| |
| No fake PYQ or official claims without evidence. |
| """ |
| from __future__ import annotations |
|
|
| import logging |
| import os |
| import re |
| import threading |
| from dataclasses import dataclass, field |
| from datetime import datetime, timezone |
| from typing import Any |
|
|
| from pydantic import BaseModel, Field |
|
|
| from app.services.syllabus_catalog import ( |
| find_syllabus_item, |
| get_items_by_subject, |
| get_prerequisites, |
| list_derivations, |
| list_numerical_patterns, |
| ) |
| from app.services.study_path_engine import KNOWN_TOPIC_PATHS, TOPIC_METADATA |
|
|
|
|
| logger = logging.getLogger(__name__) |
|
|
| |
| |
| |
| |
| |
| _PLAN_LLM_SLOTS = threading.BoundedSemaphore(2) |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class Lesson: |
| title: str |
| duration_minutes: int |
| task: str |
| reason_type: str |
| source_basis: str |
| expected_output: str = "" |
| prerequisite_check: str = "" |
|
|
|
|
| @dataclass |
| class Module: |
| title: str |
| lessons: list[Lesson] = field(default_factory=list) |
| estimated_minutes: int = 0 |
| source_basis: str = "catalog_seed" |
|
|
|
|
| @dataclass |
| class Quiz: |
| title: str |
| question_count: int |
| focus_areas: list[str] = field(default_factory=list) |
|
|
|
|
| @dataclass |
| class PracticeTask: |
| title: str |
| task_type: str |
| description: str = "" |
| source_basis: str = "generic" |
|
|
|
|
| @dataclass |
| class RevisionCheckpoint: |
| title: str |
| focus_areas: list[str] = field(default_factory=list) |
| estimated_minutes: int = 15 |
|
|
|
|
| @dataclass |
| class StudentContext: |
| class_level: str = "" |
| syllabus: str = "" |
| subject: str = "" |
| chapter: str = "" |
| exam_date: str = "" |
| daily_study_time: str = "" |
| goal: str = "" |
|
|
|
|
| @dataclass |
| class ConceptExplanation: |
| title: str |
| explanation: str |
| board_exam_focus: str = "" |
|
|
|
|
| @dataclass |
| class DerivationProblemStep: |
| title: str |
| steps: list[str] = field(default_factory=list) |
| common_mistake: str = "" |
| source_basis: str = "catalog_seed" |
|
|
|
|
| @dataclass |
| class PracticeQuestion: |
| question: str |
| marks: int = 2 |
| answer_hint: str = "" |
| question_type: str = "short_answer" |
|
|
|
|
| @dataclass |
| class WeakTopicRepair: |
| topic: str |
| symptom: str = "" |
| repair_task: str = "" |
|
|
|
|
| @dataclass |
| class RevisionPlanItem: |
| timing: str |
| task: str |
| purpose: str = "" |
| estimated_minutes: int = 10 |
|
|
|
|
| @dataclass |
| class CoursePlan: |
| title: str |
| learner_level: str |
| subject_area: str |
| source_basis: str |
| modules: list[Module] = field(default_factory=list) |
| lessons_total: int = 0 |
| estimated_total_minutes: int = 0 |
| prerequisites: list[str] = field(default_factory=list) |
| key_concepts: list[str] = field(default_factory=list) |
| practice_tasks: list[PracticeTask] = field(default_factory=list) |
| quizzes: list[Quiz] = field(default_factory=list) |
| revision_checkpoints: list[RevisionCheckpoint] = field(default_factory=list) |
| final_outcome: str = "" |
| next_action: str = "" |
| data_source_label: str = "Starter seed" |
| confidence: float = 0.0 |
| trust_notes: list[str] = field(default_factory=list) |
| student_context: StudentContext = field(default_factory=StudentContext) |
| lesson_outline: list[str] = field(default_factory=list) |
| prerequisite_check: list[str] = field(default_factory=list) |
| concept_explanation: ConceptExplanation | None = None |
| derivation_or_problem_steps: list[DerivationProblemStep] = field(default_factory=list) |
| practice_questions: list[PracticeQuestion] = field(default_factory=list) |
| pyq_style_questions: list[PracticeQuestion] = field(default_factory=list) |
| weak_topic_repairs: list[WeakTopicRepair] = field(default_factory=list) |
| revision_plan: list[RevisionPlanItem] = field(default_factory=list) |
| estimated_study_time: str = "" |
|
|
|
|
| |
| |
| |
|
|
| _COURSE_TYPE_PATTERNS: list[tuple[str, str]] = [ |
| (r"\bsemester\s*(\d+|\w+)\b", "degree"), |
| (r"\b\d+\s*(?:st|nd|rd|th)\s*sem(?:ester)?\b", "degree"), |
| (r"\bb\.?tech\b|\bbachelor\b|\bmaster\b|\bm\.?tech\b|\bmba\b|\bbca\b|\bmca\b", "degree"), |
| (r"\bplaylist\b", "playlist"), |
| (r"\bcourse\b|\blearn\b", "skill"), |
| ] |
|
|
|
|
| def _detect_course_type(raw_text: str) -> str: |
| lower = raw_text.lower() |
| for pattern, course_type in _COURSE_TYPE_PATTERNS: |
| if re.search(pattern, lower): |
| return course_type |
| return "topic" |
|
|
|
|
| def _extract_semester(raw_text: str) -> str: |
| lower = raw_text.lower() |
| match = re.search(r"(?:semester|sem)\s*(\d+|\w+)", lower) |
| if match: |
| return match.group(1) |
| match = re.search(r"(\d+)(?:st|nd|rd|th)\s*sem", lower) |
| if match: |
| return match.group(1) |
| return "" |
|
|
|
|
| def _extract_degree_subjects(raw_text: str) -> list[str]: |
| """Extract subject names from degree-level requests.""" |
| lower = raw_text.lower() |
| subjects = [] |
| known_degree_subjects = [ |
| "disaster management", "machine learning", "artificial intelligence", |
| "data structures", "algorithms", "database management", "operating systems", |
| "computer networks", "software engineering", "web development", |
| "machine learning", "deep learning", "natural language processing", |
| "computer vision", "data mining", "cloud computing", "cyber security", |
| "Internet of Things", "blockchain", "quantum computing", |
| "power systems", "control systems", "signal processing", |
| "vlsi", "embedded systems", "robotics", |
| "organic chemistry", "inorganic chemistry", "physical chemistry", |
| "biochemistry", "molecular biology", "genetics", |
| "microeconomics", "macroeconomics", "financial accounting", |
| "business management", "marketing", "human resource management", |
| ] |
| for subj in known_degree_subjects: |
| if subj in lower: |
| subjects.append(subj.title()) |
| return subjects |
|
|
|
|
| def _extract_skill_course_topics(raw_text: str) -> list[str]: |
| """Extract topic keywords from skill-course requests.""" |
| lower = raw_text.lower() |
| topics = [] |
| skill_keywords = [ |
| "machine learning", "deep learning", "neural network", "python", |
| "javascript", "react", "node", "django", "flask", "fastapi", |
| "data science", "data analysis", "statistics", "linear algebra", |
| "calculus", "probability", "regex", "html", "css", "sql", |
| "git", "docker", "kubernetes", "aws", "azure", "terraform", |
| "tensorflow", "pytorch", "scikit", "pandas", "numpy", |
| "computer vision", "nlp", "natural language processing", |
| "reinforcement learning", "transformer", "attention mechanism", |
| "gradient descent", "backpropagation", "convolutional neural network", |
| "recurrent neural network", "generative ai", "large language model", |
| "prompt engineering", "fine tuning", "transfer learning", |
| ] |
| for kw in skill_keywords: |
| if kw in lower: |
| topics.append(kw.title()) |
| return topics |
|
|
|
|
| |
| |
| |
|
|
| def _build_school_modules( |
| subject: str, |
| chapter: str, |
| source_context: str, |
| has_source: bool, |
| time_available: str, |
| ) -> list[Module]: |
| """Build modules for school-level topic (class/board/subject).""" |
| subject_lower = subject.lower() |
| topic_lower = (chapter or subject).lower() |
| modules: list[Module] = [] |
|
|
| |
| catalog_item = find_syllabus_item(chapter or subject) |
| if catalog_item: |
| source_label = "Uploaded source" if has_source else "Catalog seed" |
| lessons = _catalog_item_to_lessons(catalog_item, source_label) |
| modules.append(Module( |
| title=catalog_item.get("topic", chapter or subject), |
| lessons=lessons, |
| estimated_minutes=sum(l.duration_minutes for l in lessons), |
| source_basis=source_label, |
| )) |
| return modules |
|
|
| |
| topic_path = KNOWN_TOPIC_PATHS.get(topic_lower, []) |
| if topic_path: |
| source_label = "Uploaded source" if has_source else "Catalog seed" |
| lessons = [ |
| Lesson( |
| title=step["title"], |
| duration_minutes=20, |
| task=step["task"], |
| reason_type=step.get("reason_type", "concept"), |
| source_basis=source_label, |
| ) |
| for step in topic_path |
| ] |
| modules.append(Module( |
| title=topic.title(), |
| lessons=lessons, |
| estimated_minutes=sum(l.duration_minutes for l in lessons), |
| source_basis=source_label, |
| )) |
| return modules |
|
|
| |
| source_label = "Uploaded source" if has_source else "Generic template" |
| lessons = _generic_lessons(topic_lower, subject_lower, source_label) |
| display_title = (chapter or subject or "Course").title() |
| modules.append(Module( |
| title=display_title, |
| lessons=lessons, |
| estimated_minutes=sum(l.duration_minutes for l in lessons), |
| source_basis=source_label, |
| )) |
| return modules |
|
|
|
|
| def _build_degree_modules( |
| subject_area: str, |
| semester: str, |
| source_context: str, |
| has_source: bool, |
| ) -> list[Module]: |
| """Build modules for degree-level semester subjects.""" |
| modules: list[Module] = [] |
| source_label = "Uploaded source" if has_source else "Catalog seed" |
|
|
| |
| subject_lower = subject_area.lower() |
|
|
| |
| catalog_item = find_syllabus_item(subject_area) |
| if catalog_item: |
| lessons = _catalog_item_to_lessons(catalog_item, source_label) |
| modules.append(Module( |
| title=catalog_item.get("topic", subject_area), |
| lessons=lessons, |
| estimated_minutes=sum(l.duration_minutes for l in lessons), |
| source_basis=source_label, |
| )) |
| return modules |
|
|
| |
| modules = _build_generic_degree_modules(subject_area, semester, source_label) |
| return modules |
|
|
|
|
| def _build_generic_degree_modules( |
| subject_area: str, |
| semester: str, |
| source_label: str, |
| ) -> list[Module]: |
| """Build generic modules for a degree subject.""" |
| modules: list[Module] = [] |
|
|
| |
| foundation_lessons = [ |
| Lesson( |
| title=f"Introduction to {subject_area}", |
| duration_minutes=20, |
| task=f"Read the overview and core definitions of {subject_area}", |
| reason_type="concept", |
| source_basis=source_label, |
| expected_output=f"One-paragraph summary of what {subject_area} covers", |
| ), |
| Lesson( |
| title=f"Key terminology in {subject_area}", |
| duration_minutes=15, |
| task=f"List and define 10-15 key terms used in {subject_area}", |
| reason_type="exam_keyword", |
| source_basis=source_label, |
| expected_output="Terminology card with definitions", |
| ), |
| ] |
| modules.append(Module( |
| title=f"{subject_area} — Foundations", |
| lessons=foundation_lessons, |
| estimated_minutes=sum(l.duration_minutes for l in foundation_lessons), |
| source_basis=source_label, |
| )) |
|
|
| |
| core_lessons = [ |
| Lesson( |
| title=f"Core principles of {subject_area}", |
| duration_minutes=30, |
| task=f"Study the fundamental principles, theories, and frameworks of {subject_area}", |
| reason_type="core_concept", |
| source_basis=source_label, |
| expected_output="Summary of 5-7 core principles with examples", |
| ), |
| Lesson( |
| title=f"Important definitions and formulas", |
| duration_minutes=20, |
| task=f"Extract and memorize key formulas, definitions, and mathematical relationships", |
| reason_type="exam_keyword", |
| source_basis=source_label, |
| expected_output="Formula card ready for exam", |
| ), |
| ] |
| modules.append(Module( |
| title=f"{subject_area} — Core Concepts", |
| lessons=core_lessons, |
| estimated_minutes=sum(l.duration_minutes for l in core_lessons), |
| source_basis=source_label, |
| )) |
|
|
| |
| application_lessons = [ |
| Lesson( |
| title=f"Real-world applications of {subject_area}", |
| duration_minutes=25, |
| task=f"Study case studies, real-world examples, and applications of {subject_area}", |
| reason_type="application", |
| source_basis=source_label, |
| expected_output="3-5 application examples with context", |
| ), |
| Lesson( |
| title=f"Problem-solving in {subject_area}", |
| duration_minutes=30, |
| task=f"Solve practice problems and numerical examples related to {subject_area}", |
| reason_type="numerical", |
| source_basis=source_label, |
| expected_output="Solved examples with step-by-step approach", |
| ), |
| ] |
| modules.append(Module( |
| title=f"{subject_area} — Applications & Practice", |
| lessons=application_lessons, |
| estimated_minutes=sum(l.duration_minutes for l in application_lessons), |
| source_basis=source_label, |
| )) |
|
|
| |
| exam_lessons = [ |
| Lesson( |
| title=f"Exam-style answers for {subject_area}", |
| duration_minutes=25, |
| task=f"Practice writing board/exam-style answers for {subject_area} topics", |
| reason_type="answer_writing", |
| source_basis=source_label, |
| expected_output="2-3 exam-style answers ready to reproduce", |
| ), |
| Lesson( |
| title=f"Revision and self-test", |
| duration_minutes=15, |
| task=f"Quick revision of all key points and self-assessment quiz", |
| reason_type="revision", |
| source_basis=source_label, |
| expected_output="Confidence check — ready for exam", |
| ), |
| ] |
| modules.append(Module( |
| title=f"{subject_area} — Exam Preparation", |
| lessons=exam_lessons, |
| estimated_minutes=sum(l.duration_minutes for l in exam_lessons), |
| source_basis=source_label, |
| )) |
|
|
| return modules |
|
|
|
|
| def _build_skill_modules( |
| topics: list[str], |
| source_context: str, |
| has_source: bool, |
| ) -> list[Module]: |
| """Build modules for skill-based courses (ML, web dev, etc.).""" |
| modules: list[Module] = [] |
| source_label = "Uploaded source" if has_source else "Catalog seed" |
|
|
| primary_topic = topics[0] if topics else "the topic" |
|
|
| |
| prereq_lessons = [ |
| Lesson( |
| title=f"Prerequisites for {primary_topic}", |
| duration_minutes=20, |
| task=f"Review mathematical and programming prerequisites needed for {primary_topic}", |
| reason_type="concept", |
| source_basis=source_label, |
| expected_output="Prerequisite checklist — confirm readiness", |
| ), |
| Lesson( |
| title=f"Environment setup and tools", |
| duration_minutes=15, |
| task="Set up the development environment, install required libraries and tools", |
| reason_type="concept", |
| source_basis=source_label, |
| expected_output="Working environment ready", |
| ), |
| ] |
| modules.append(Module( |
| title=f"{primary_topic} — Setup & Prerequisites", |
| lessons=prereq_lessons, |
| estimated_minutes=sum(l.duration_minutes for l in prereq_lessons), |
| source_basis=source_label, |
| )) |
|
|
| |
| theory_lessons = [ |
| Lesson( |
| title=f"Core theory of {primary_topic}", |
| duration_minutes=30, |
| task=f"Study the fundamental concepts, algorithms, and theory behind {primary_topic}", |
| reason_type="core_concept", |
| source_basis=source_label, |
| expected_output="Summary of core theory with key equations/concepts", |
| ), |
| Lesson( |
| title=f"Key terminology and math", |
| duration_minutes=20, |
| task=f"Define key terms and review the mathematical foundations for {primary_topic}", |
| reason_type="exam_keyword", |
| source_basis=source_label, |
| expected_output="Terminology and math reference card", |
| ), |
| ] |
| modules.append(Module( |
| title=f"{primary_topic} — Core Theory", |
| lessons=theory_lessons, |
| estimated_minutes=sum(l.duration_minutes for l in theory_lessons), |
| source_basis=source_label, |
| )) |
|
|
| |
| practice_lessons = [ |
| Lesson( |
| title=f"Coding exercises for {primary_topic}", |
| duration_minutes=40, |
| task=f"Implement basic examples and coding exercises for {primary_topic}", |
| reason_type="practice", |
| source_basis=source_label, |
| expected_output="Working code examples with explanations", |
| ), |
| Lesson( |
| title=f"Mini project: {primary_topic}", |
| duration_minutes=45, |
| task=f"Build a small project applying {primary_topic} concepts end-to-end", |
| reason_type="practice", |
| source_basis=source_label, |
| expected_output="Complete mini-project with results", |
| ), |
| ] |
| modules.append(Module( |
| title=f"{primary_topic} — Hands-on Practice", |
| lessons=practice_lessons, |
| estimated_minutes=sum(l.duration_minutes for l in practice_lessons), |
| source_basis=source_label, |
| )) |
|
|
| |
| assessment_lessons = [ |
| Lesson( |
| title=f"Quiz on {primary_topic}", |
| duration_minutes=15, |
| task=f"Test understanding with a quiz covering all {primary_topic} concepts", |
| reason_type="revision", |
| source_basis=source_label, |
| expected_output="Quiz score and areas to review", |
| ), |
| Lesson( |
| title=f"Next steps in {primary_topic}", |
| duration_minutes=10, |
| task=f"Identify advanced topics and next learning path after {primary_topic}", |
| reason_type="revision", |
| source_basis=source_label, |
| expected_output="Learning roadmap for advanced study", |
| ), |
| ] |
| modules.append(Module( |
| title=f"{primary_topic} — Assessment & Next Steps", |
| lessons=assessment_lessons, |
| estimated_minutes=sum(l.duration_minutes for l in assessment_lessons), |
| source_basis=source_label, |
| )) |
|
|
| return modules |
|
|
|
|
| def _extract_playlist_topics( |
| raw_request: str, |
| playlist_metadata: dict[str, Any], |
| fallback_topic: str, |
| ) -> list[str]: |
| """Extract deterministic topic hints for playlist-based learning plans.""" |
| candidates: list[Any] = [] |
| for key in ("topics", "chapters", "sections", "titles"): |
| value = playlist_metadata.get(key) |
| if isinstance(value, list): |
| candidates.extend(value) |
|
|
| topics: list[str] = [] |
| for item in candidates: |
| if isinstance(item, str): |
| cleaned = item.strip() |
| elif isinstance(item, dict): |
| cleaned = str( |
| item.get("title") |
| or item.get("topic") |
| or item.get("name") |
| or "" |
| ).strip() |
| else: |
| cleaned = "" |
| if cleaned and cleaned not in topics: |
| topics.append(cleaned) |
|
|
| if topics: |
| return topics[:6] |
|
|
| skill_topics = _extract_skill_course_topics(raw_request) |
| if skill_topics: |
| return skill_topics[:6] |
|
|
| return [fallback_topic or "Playlist course"] |
|
|
|
|
| def _catalog_item_to_lessons(item: dict, source_label: str) -> list[Lesson]: |
| """Convert a catalog item into a list of lessons.""" |
| lessons: list[Lesson] = [] |
| topic = item.get("topic", "") |
|
|
| |
| lessons.append(Lesson( |
| title=f"Understanding {topic}", |
| duration_minutes=20, |
| task=f"Read and understand the core concepts of {topic}", |
| reason_type="concept", |
| source_basis=source_label, |
| expected_output=f"One-paragraph explanation of {topic}", |
| )) |
|
|
| |
| if item.get("derivations"): |
| for derivation in item["derivations"][:2]: |
| name = derivation if isinstance(derivation, str) else str(derivation) |
| lessons.append(Lesson( |
| title=f"Derivation: {name[:60]}", |
| duration_minutes=25, |
| task=f"Study the derivation step by step: {name[:80]}", |
| reason_type="derivation", |
| source_basis=source_label, |
| expected_output=f"Complete derivation with final boxed formula", |
| )) |
|
|
| |
| if item.get("formulas"): |
| lessons.append(Lesson( |
| title=f"Key formulas for {topic}", |
| duration_minutes=15, |
| task="Memorize and understand key formulas and their conditions", |
| reason_type="exam_keyword", |
| source_basis=source_label, |
| expected_output="Formula card with conditions and units", |
| )) |
|
|
| |
| if item.get("numerical_patterns"): |
| lessons.append(Lesson( |
| title=f"Numerical practice for {topic}", |
| duration_minutes=25, |
| task="Practice solving numerical problems using the formulas", |
| reason_type="numerical", |
| source_basis=source_label, |
| expected_output="Solved numericals with method", |
| )) |
|
|
| |
| lessons.append(Lesson( |
| title=f"Board answer practice for {topic}", |
| duration_minutes=20, |
| task="Practice writing exam-style answers for 2-mark, 3-mark, and 5-mark questions", |
| reason_type="answer_writing", |
| source_basis=source_label, |
| expected_output="2-3 board-ready answers", |
| )) |
|
|
| |
| lessons.append(Lesson( |
| title=f"Revision checkpoint: {topic}", |
| duration_minutes=10, |
| task="Quick revision of all key points, common mistakes, and important questions", |
| reason_type="revision", |
| source_basis=source_label, |
| expected_output="Self-test checklist", |
| )) |
|
|
| return lessons |
|
|
|
|
| def _generic_lessons(topic: str, subject: str, source_label: str) -> list[Lesson]: |
| """Build generic lessons for any topic.""" |
| return [ |
| Lesson( |
| title=f"Introduction to {topic.title()}", |
| duration_minutes=20, |
| task=f"Read the basic definition and overview of {topic}", |
| reason_type="concept", |
| source_basis=source_label, |
| expected_output=f"One-line definition of {topic}", |
| ), |
| Lesson( |
| title=f"Core ideas of {topic.title()}", |
| duration_minutes=25, |
| task=f"List 3-5 must-learn points about {topic}", |
| reason_type="core_concept", |
| source_basis=source_label, |
| expected_output="3-5 bullet points ready for exam", |
| ), |
| Lesson( |
| title=f"Exam keywords and formulas", |
| duration_minutes=15, |
| task="Underline keywords and write formulas on one card", |
| reason_type="exam_keyword", |
| source_basis=source_label, |
| expected_output="Keyword card with 6-8 items", |
| ), |
| Lesson( |
| title=f"Practice answers for {topic.title()}", |
| duration_minutes=20, |
| task="Write 1-mark, 2-mark, and 4-mark answers", |
| reason_type="answer_writing", |
| source_basis=source_label, |
| expected_output="Drafted exam answers", |
| ), |
| Lesson( |
| title=f"Revision sweep", |
| duration_minutes=10, |
| task="Quick check + note common mistakes", |
| reason_type="revision", |
| source_basis=source_label, |
| expected_output="Confidence self-test", |
| ), |
| ] |
|
|
|
|
| |
| |
| |
|
|
| def _build_prerequisites(subject: str, topic: str) -> list[str]: |
| """Get prerequisites from catalog or generate generic ones.""" |
| catalog_prereqs = get_prerequisites(topic) |
| if catalog_prereqs: |
| return catalog_prereqs |
|
|
| subject_lower = subject.lower() |
| if subject_lower == "physics": |
| return ["Basic algebra", "Vector concepts", "SI units and measurements"] |
| if subject_lower == "chemistry": |
| return ["Atomic structure basics", "Periodic table awareness", "Basic math"] |
| if subject_lower in {"mathematics", "maths", "math"}: |
| return ["Basic algebra", "Functions and graphs", "Set theory basics"] |
| return [f"Basic understanding of {subject} fundamentals"] |
|
|
|
|
| def _build_key_concepts(subject: str, topic: str, modules: list[Module]) -> list[str]: |
| """Extract key concepts from modules.""" |
| concepts: list[str] = [] |
| for module in modules: |
| for lesson in module.lessons: |
| if lesson.reason_type in {"concept", "core_concept", "exam_keyword"}: |
| if lesson.title not in concepts: |
| concepts.append(lesson.title) |
| return concepts[:10] |
|
|
|
|
| def _build_practice_tasks(subject: str, topic: str, modules: list[Module]) -> list[PracticeTask]: |
| """Build practice tasks based on subject and modules.""" |
| tasks: list[PracticeTask] = [] |
| subject_lower = subject.lower() |
|
|
| if subject_lower == "physics": |
| tasks.append(PracticeTask( |
| title=f"Derivation practice: {topic}", |
| task_type="derivation", |
| description=f"Derive key formulas related to {topic} with proper assumptions and unit checks", |
| source_basis="catalog_seed", |
| )) |
| tasks.append(PracticeTask( |
| title=f"Numerical problems: {topic}", |
| task_type="numerical", |
| description=f"Solve 3-5 numerical problems from {topic} using given-formula-substitution method", |
| source_basis="catalog_seed", |
| )) |
| elif subject_lower == "chemistry": |
| tasks.append(PracticeTask( |
| title=f"Numerical calculations: {topic}", |
| task_type="numerical", |
| description=f"Practice mole concept, stoichiometry, and numerical problems from {topic}", |
| source_basis="catalog_seed", |
| )) |
| elif subject_lower in {"mathematics", "maths", "math"}: |
| tasks.append(PracticeTask( |
| title=f"Proof practice: {topic}", |
| task_type="derivation", |
| description=f"Prove key theorems and identities related to {topic} step by step", |
| source_basis="catalog_seed", |
| )) |
| else: |
| tasks.append(PracticeTask( |
| title=f"Board answer practice: {topic}", |
| task_type="board_answer", |
| description=f"Write 2-mark, 3-mark, and 5-mark board answers for {topic}", |
| source_basis="catalog_seed", |
| )) |
|
|
| return tasks |
|
|
|
|
| def _build_quizzes(topic: str, modules: list[Module]) -> list[Quiz]: |
| """Build quizzes for the course.""" |
| quizzes: list[Quiz] = [] |
| lesson_titles = [] |
| for module in modules: |
| for lesson in module.lessons: |
| if lesson.reason_type in {"concept", "core_concept", "exam_keyword"}: |
| lesson_titles.append(lesson.title) |
|
|
| if lesson_titles: |
| quizzes.append(Quiz( |
| title=f"Module quiz: {topic}", |
| question_count=min(10, len(lesson_titles) * 2), |
| focus_areas=lesson_titles[:5], |
| )) |
|
|
| quizzes.append(Quiz( |
| title=f"Final assessment: {topic}", |
| question_count=15, |
| focus_areas=lesson_titles[:8], |
| )) |
|
|
| return quizzes |
|
|
|
|
| def _build_revision_checkpoints(topic: str, modules: list[Module]) -> list[RevisionCheckpoint]: |
| """Build revision checkpoints.""" |
| checkpoints: list[RevisionCheckpoint] = [] |
|
|
| for i, module in enumerate(modules, 1): |
| focus = [l.title for l in module.lessons if l.reason_type in {"concept", "core_concept"}][:3] |
| checkpoints.append(RevisionCheckpoint( |
| title=f"After {module.title}", |
| focus_areas=focus, |
| estimated_minutes=10, |
| )) |
|
|
| return checkpoints |
|
|
|
|
| def _first_non_empty(*values: str | None) -> str: |
| for value in values: |
| if value and value.strip(): |
| return value.strip() |
| return "" |
|
|
|
|
| def _format_estimated_study_time(minutes: int, daily_study_time: str) -> str: |
| if daily_study_time: |
| return f"{minutes} minutes total, paced around {daily_study_time} per day" |
| if minutes >= 120: |
| return f"{minutes} minutes total, best split across 2-3 study sessions" |
| if minutes >= 60: |
| return f"{minutes} minutes total, finishable in one focused evening" |
| return f"{minutes} minutes total" |
|
|
|
|
| def _build_student_context( |
| *, |
| class_level: str, |
| syllabus: str, |
| board: str, |
| subject: str, |
| chapter: str, |
| topic: str, |
| exam_date: str, |
| daily_study_time: str, |
| time_available: str, |
| goal: str, |
| ) -> StudentContext: |
| return StudentContext( |
| class_level=class_level, |
| syllabus=_first_non_empty(syllabus, board), |
| subject=subject, |
| chapter=_first_non_empty(chapter, topic), |
| exam_date=exam_date, |
| daily_study_time=_first_non_empty(daily_study_time, time_available), |
| goal=goal, |
| ) |
|
|
|
|
| def _build_lesson_outline(modules: list[Module]) -> list[str]: |
| outline: list[str] = [] |
| for module in modules: |
| for lesson in module.lessons: |
| outline.append(f"{lesson.title}: {lesson.task}") |
| return outline[:10] |
|
|
|
|
| def _build_prerequisite_check(subject: str, topic: str, prerequisites: list[str]) -> list[str]: |
| checks = [f"Can you explain {item} in one line?" for item in prerequisites[:4]] |
| subject_lower = subject.lower() |
| if subject_lower == "physics": |
| checks.append("Can you identify the variables, SI units, and formula conditions before solving?") |
| elif subject_lower == "chemistry": |
| checks.append("Can you write the given data, formula, substitution, and final unit clearly?") |
| elif subject_lower in {"mathematics", "maths", "math"}: |
| checks.append("Can you state the given, to-prove, and reason for each step?") |
| if not checks: |
| checks.append(f"Can you state the basic meaning of {topic} before starting?") |
| return checks |
|
|
|
|
| def _build_concept_explanation(subject: str, topic: str, modules: list[Module]) -> ConceptExplanation: |
| first_concepts = [ |
| lesson.title |
| for module in modules |
| for lesson in module.lessons |
| if lesson.reason_type in {"concept", "core_concept"} |
| ][:3] |
| concept_list = ", ".join(first_concepts) if first_concepts else topic |
| subject_label = subject or "this subject" |
| return ConceptExplanation( |
| title=f"Teach from zero: {topic}", |
| explanation=( |
| f"Start with the meaning of {topic} in {subject_label}, then connect it to " |
| f"{concept_list}. DocDoe should first remove confusion, then move to exam wording." |
| ), |
| board_exam_focus=( |
| "Write definitions first, underline scoring keywords, then add formula, diagram, " |
| "or example only when the question asks for it." |
| ), |
| ) |
|
|
|
|
| def _build_derivation_or_problem_steps( |
| subject: str, |
| topic: str, |
| modules: list[Module], |
| source_basis: str, |
| ) -> list[DerivationProblemStep]: |
| subject_lower = subject.lower() |
| lesson_titles = [ |
| lesson.title |
| for module in modules |
| for lesson in module.lessons |
| if lesson.reason_type in {"derivation", "numerical", "answer_writing", "core_concept"} |
| ][:2] |
| if not lesson_titles: |
| lesson_titles = [topic] |
|
|
| if subject_lower == "physics": |
| steps = [ |
| "Write the physical meaning and known quantities.", |
| "State the formula or relation with symbols defined.", |
| "Show derivation or substitution step by step.", |
| "Check units and box the final answer.", |
| ] |
| mistake = "Students often skip symbol definitions or unit checks." |
| elif subject_lower == "chemistry": |
| steps = [ |
| "Write given data and balanced equation or formula.", |
| "Convert units or moles before substitution.", |
| "Substitute carefully and show the final unit.", |
| "Mention conditions when reactions or mechanisms are involved.", |
| ] |
| mistake = "Students often lose marks by skipping units, conditions, or balanced equations." |
| elif subject_lower in {"mathematics", "maths", "math"}: |
| steps = [ |
| "Write given and to-prove clearly.", |
| "Use the correct theorem or identity.", |
| "Show every algebra step with a reason.", |
| "End with the final result exactly as required.", |
| ] |
| mistake = "Students often jump steps and lose reasoning marks." |
| else: |
| steps = [ |
| "Define the concept.", |
| "Add the main points in order.", |
| "Support with one example.", |
| "Finish with a short exam-ready conclusion.", |
| ] |
| mistake = "Students often write a general paragraph without scoring keywords." |
|
|
| return [ |
| DerivationProblemStep( |
| title=title, |
| steps=steps, |
| common_mistake=mistake, |
| source_basis=source_basis, |
| ) |
| for title in lesson_titles |
| ] |
|
|
|
|
| def _build_practice_questions_for_tuition(topic: str, subject: str) -> list[PracticeQuestion]: |
| subject_lower = subject.lower() |
| if subject_lower == "physics": |
| return [ |
| PracticeQuestion( |
| question=f"Define the key principle behind {topic} and mention one SI unit involved.", |
| marks=2, |
| answer_hint="Definition + symbol/unit + one condition.", |
| question_type="short_answer", |
| ), |
| PracticeQuestion( |
| question=f"Solve a numerical or derivation-style problem from {topic} using full steps.", |
| marks=5, |
| answer_hint="Given, formula, substitution/derivation, unit check, boxed answer.", |
| question_type="derivation_or_numerical", |
| ), |
| ] |
| if subject_lower == "chemistry": |
| return [ |
| PracticeQuestion( |
| question=f"Write the formula or equation used in {topic} and define each term.", |
| marks=2, |
| answer_hint="Formula/equation + terms + units or conditions.", |
| question_type="short_answer", |
| ), |
| PracticeQuestion( |
| question=f"Solve one calculation from {topic} with formula, substitution, and final unit.", |
| marks=4, |
| answer_hint="Given, formula, substitution, calculation, unit.", |
| question_type="numerical", |
| ), |
| ] |
| if subject_lower in {"mathematics", "maths", "math"}: |
| return [ |
| PracticeQuestion( |
| question=f"State the theorem or formula needed for {topic}.", |
| marks=2, |
| answer_hint="Statement + condition + notation.", |
| question_type="short_answer", |
| ), |
| PracticeQuestion( |
| question=f"Prove or solve a board-style problem from {topic} with reasons.", |
| marks=5, |
| answer_hint="Given, to-prove, steps, reasons, final result.", |
| question_type="proof_or_problem", |
| ), |
| ] |
| return [ |
| PracticeQuestion( |
| question=f"Write a 2-mark answer explaining {topic}.", |
| marks=2, |
| answer_hint="Definition + two scoring keywords.", |
| question_type="short_answer", |
| ), |
| PracticeQuestion( |
| question=f"Write a 5-mark board answer on {topic} with structure.", |
| marks=5, |
| answer_hint="Intro, main points, example, conclusion.", |
| question_type="board_answer", |
| ), |
| ] |
|
|
|
|
| def _build_pyq_style_questions(topic: str, subject: str, has_source: bool) -> list[PracticeQuestion]: |
| source_note = "based on selected source pattern" if has_source else "practice style, not an official PYQ claim" |
| return [ |
| PracticeQuestion( |
| question=f"PYQ-style: Explain {topic} in board-exam format ({source_note}).", |
| marks=3, |
| answer_hint="Use definition, keywords, and one example or formula.", |
| question_type="pyq_style", |
| ), |
| PracticeQuestion( |
| question=f"PYQ-style: Apply {topic} to a short problem or case ({source_note}).", |
| marks=5, |
| answer_hint="Show ordered steps and mark-scoring terms.", |
| question_type="pyq_style", |
| ), |
| ] |
|
|
|
|
| def _build_weak_topic_repairs(subject: str, topic: str, prerequisites: list[str]) -> list[WeakTopicRepair]: |
| repairs = [ |
| WeakTopicRepair( |
| topic=item, |
| symptom=f"If {topic} feels confusing, this prerequisite may be weak.", |
| repair_task=f"Revise {item} for 10 minutes, then explain it aloud in one sentence.", |
| ) |
| for item in prerequisites[:3] |
| ] |
| subject_lower = subject.lower() |
| if subject_lower == "physics": |
| repairs.append(WeakTopicRepair( |
| topic="Formula selection", |
| symptom="You know the topic but freeze when solving questions.", |
| repair_task="Make a two-column card: condition on left, formula on right.", |
| )) |
| elif subject_lower == "chemistry": |
| repairs.append(WeakTopicRepair( |
| topic="Units and equations", |
| symptom="Final answer is close but marks are lost in presentation.", |
| repair_task="Practice given-formula-substitution-unit format on two examples.", |
| )) |
| elif subject_lower in {"mathematics", "maths", "math"}: |
| repairs.append(WeakTopicRepair( |
| topic="Reasoning steps", |
| symptom="Answer reaches the result but proof marks are missing.", |
| repair_task="Write one reason beside every transformation step.", |
| )) |
| return repairs |
|
|
|
|
| def _build_revision_plan( |
| topic: str, |
| daily_study_time: str, |
| time_available: str, |
| estimated_minutes: int, |
| ) -> list[RevisionPlanItem]: |
| time_label = _first_non_empty(daily_study_time, time_available, "1 hour") |
| return [ |
| RevisionPlanItem( |
| timing="Start", |
| task=f"Prerequisite check for {topic}", |
| purpose="Find the weak link before studying the chapter.", |
| estimated_minutes=10, |
| ), |
| RevisionPlanItem( |
| timing=f"Main block ({time_label})", |
| task=f"Learn concept, steps, and board answer structure for {topic}", |
| purpose="Move from understanding to exam-ready writing.", |
| estimated_minutes=max(20, min(estimated_minutes - 20, 60)), |
| ), |
| RevisionPlanItem( |
| timing="End", |
| task="Attempt two practice questions and mark missing keywords.", |
| purpose="Convert the lesson into score-ready recall.", |
| estimated_minutes=15, |
| ), |
| ] |
|
|
|
|
| |
| |
| |
|
|
| class _LlmLessonSchema(BaseModel): |
| title: str |
| duration_minutes: int = 25 |
| task: str = "" |
|
|
|
|
| class _LlmModuleSchema(BaseModel): |
| title: str |
| lessons: list[_LlmLessonSchema] = Field(default_factory=list) |
|
|
|
|
| class _LlmCourseSchema(BaseModel): |
| modules: list[_LlmModuleSchema] = Field(default_factory=list) |
|
|
|
|
| def _generate_llm_modules_unbounded( |
| *, |
| topic: str, |
| current_level: str, |
| goal: str, |
| time_available: str, |
| daily_study_time: str, |
| source_context: str = "", |
| ) -> list[Module] | None: |
| """Personalised multi-week modules from the configured AI provider. |
| |
| Returns None whenever the provider is unavailable, errors, or returns an |
| unusable structure — the caller then falls back to the deterministic seed |
| builder, so a student always gets an honest plan and never broken JSON. |
| """ |
| try: |
| from app.services.ai_provider import get_ai_provider |
|
|
| provider = get_ai_provider() |
| except Exception: |
| return None |
|
|
| task = ( |
| "Design a realistic, personalised learning course for this learner as JSON. " |
| "Rules: 4-10 modules ordered from fundamentals to applied work, each with " |
| "3-6 lessons. Every lesson needs a concrete hands-on task the learner can " |
| "actually do (never just 'watch a video'). Size the whole course honestly " |
| "for the learner's stated timeline and weekly time — do not promise mastery " |
| "that does not fit. Later modules must build on earlier ones. Plain, natural " |
| "teaching language. Respond with JSON only, no commentary, in exactly this " |
| "format (this is an example of the STRUCTURE, write your own content): " |
| '{"modules": [{"title": "Foundations of Python", "lessons": [' |
| '{"title": "Variables and types", "duration_minutes": 25, ' |
| '"task": "Write a script that stores your name, age and city in variables and prints a sentence using them."}]}]}' |
| ) |
| context_lines = [ |
| f"Topic: {topic}", |
| f"Learner level: {current_level or 'complete beginner'}", |
| f"Outcome wanted: {goal or 'understand the basics well'}", |
| f"Timeline: {time_available or 'about 3 months'}", |
| f"Weekly time: {daily_study_time or 'about 5 hours per week'}", |
| ] |
| if source_context.strip(): |
| context_lines.append(f"Learner's own material excerpt:\n{source_context[:1500]}") |
|
|
| try: |
| data = provider.generate_json( |
| task=task, |
| context="\n".join(context_lines), |
| language="English", |
| response_schema=_LlmCourseSchema, |
| ) |
| except Exception: |
| return None |
|
|
| raw_modules = data.get("modules") if isinstance(data, dict) else None |
| if not isinstance(raw_modules, list): |
| return None |
|
|
| modules: list[Module] = [] |
| for raw_module in raw_modules[:12]: |
| if not isinstance(raw_module, dict): |
| continue |
| title = str(raw_module.get("title") or "").strip() |
| raw_lessons = raw_module.get("lessons") |
| if not title or not isinstance(raw_lessons, list): |
| continue |
| lessons: list[Lesson] = [] |
| for raw_lesson in raw_lessons[:8]: |
| if not isinstance(raw_lesson, dict): |
| continue |
| lesson_title = str(raw_lesson.get("title") or "").strip() |
| lesson_task = str(raw_lesson.get("task") or "").strip() |
| if not lesson_title: |
| continue |
| try: |
| duration = int(raw_lesson.get("duration_minutes") or 25) |
| except (TypeError, ValueError): |
| duration = 25 |
| lessons.append( |
| Lesson( |
| title=lesson_title, |
| duration_minutes=max(10, min(120, duration)), |
| task=lesson_task or f"Apply {lesson_title} in one small exercise.", |
| reason_type="concept", |
| source_basis="ai_generated", |
| ) |
| ) |
| if lessons: |
| modules.append( |
| Module( |
| title=title, |
| lessons=lessons, |
| estimated_minutes=sum(lesson.duration_minutes for lesson in lessons), |
| source_basis="ai_generated", |
| ) |
| ) |
|
|
| |
| total_lessons = sum(len(module.lessons) for module in modules) |
| if len(modules) < 2 or total_lessons < 6: |
| return None |
| return modules |
|
|
|
|
| def _plan_ai_timeout_seconds() -> float: |
| raw = os.getenv("LEARN_ANYTHING_PLAN_TIMEOUT_SECONDS", "12") |
| try: |
| configured = float(raw) |
| except (TypeError, ValueError): |
| configured = 12.0 |
| |
| |
| |
| return max(1.0, min(configured, 30.0)) |
|
|
|
|
| def _generate_llm_modules( |
| *, |
| topic: str, |
| current_level: str, |
| goal: str, |
| time_available: str, |
| daily_study_time: str, |
| source_context: str = "", |
| ) -> list[Module] | None: |
| """Try AI personalisation within a hard request budget. |
| |
| Provider SDK calls are synchronous and some third-party clients do not |
| reliably honour cancellation. Run the optional enhancement in a bounded |
| daemon worker so the API request can always return the deterministic, |
| source-labelled course seed when the provider is slow or unavailable. |
| """ |
| if not _PLAN_LLM_SLOTS.acquire(blocking=False): |
| logger.info("Course-plan AI capacity is busy; using deterministic seed") |
| return None |
|
|
| result: list[Module] | None = None |
| error: Exception | None = None |
|
|
| def run() -> None: |
| nonlocal result, error |
| try: |
| result = _generate_llm_modules_unbounded( |
| topic=topic, |
| current_level=current_level, |
| goal=goal, |
| time_available=time_available, |
| daily_study_time=daily_study_time, |
| source_context=source_context, |
| ) |
| except Exception as exc: |
| error = exc |
| finally: |
| _PLAN_LLM_SLOTS.release() |
|
|
| worker = threading.Thread(target=run, name="docdoe-course-plan-ai", daemon=True) |
| worker.start() |
| worker.join(timeout=_plan_ai_timeout_seconds()) |
| if worker.is_alive(): |
| logger.warning( |
| "Course-plan AI exceeded %.1fs SLA; returning deterministic seed", |
| _plan_ai_timeout_seconds(), |
| ) |
| return None |
| if error is not None: |
| logger.info("Course-plan AI failed; returning deterministic seed: %s", type(error).__name__) |
| return None |
| return result |
|
|
|
|
| def build_course_plan( |
| *, |
| raw_request: str, |
| source_ids: list[str] | None = None, |
| source_context: str = "", |
| playlist_metadata: dict[str, Any] | None = None, |
| current_level: str = "", |
| weak_topics: list[str] | None = None, |
| class_level: str = "", |
| syllabus: str = "", |
| board: str = "", |
| semester: str = "", |
| degree: str = "", |
| goal: str = "", |
| time_available: str = "", |
| daily_study_time: str = "", |
| subject: str = "", |
| chapter: str = "", |
| topic: str = "", |
| exam_date: str = "", |
| ) -> CoursePlan: |
| """Build a structured course plan from a raw user request. |
| |
| This is a deterministic builder — no AI calls. It uses catalog data, |
| known topic paths, and generic scaffolding. |
| """ |
| source_ids = source_ids or [] |
| playlist_metadata = playlist_metadata or {} |
| weak_topics = weak_topics or [] |
| board = _first_non_empty(syllabus, board) |
| daily_study_time = _first_non_empty(daily_study_time, time_available) |
|
|
| has_source = bool(source_context.strip()) |
| has_source_ids = bool(source_ids) |
|
|
| |
| course_type = _detect_course_type(raw_request) |
| extracted_semester = semester or _extract_semester(raw_request) |
|
|
| |
| subject = subject or "" |
| chapter = chapter or "" |
| topic = _first_non_empty(topic, chapter) |
|
|
| if not subject: |
| |
| degree_subjects = _extract_degree_subjects(raw_request) |
| if degree_subjects: |
| subject = degree_subjects[0] |
| if not topic: |
| topic = degree_subjects[0] |
|
|
| if not topic: |
| |
| skill_topics = _extract_skill_course_topics(raw_request) |
| if skill_topics: |
| topic = skill_topics[0] |
| if not subject: |
| subject = skill_topics[0] |
|
|
| if not subject and not topic: |
| |
| cleaned = re.sub( |
| r"\b(teach|me|please|about|for|with|from|the|a|an|i|want|to|learn|course|semester|degree|class|board|exam|subject|goal|time)\b", |
| " ", |
| raw_request.lower(), |
| ) |
| cleaned = re.sub(r"\s+", " ", cleaned).strip() |
| if cleaned: |
| topic = cleaned.title() |
| subject = cleaned.title() |
|
|
| if not subject: |
| subject = topic or "General" |
| if not topic: |
| topic = subject |
| if not chapter: |
| chapter = topic |
|
|
| |
| |
| |
| llm_generated = False |
| if course_type in {"skill", "topic", "playlist"}: |
| llm_modules = _generate_llm_modules( |
| topic=topic or subject or raw_request[:80], |
| current_level=current_level, |
| goal=goal, |
| time_available=time_available, |
| daily_study_time=daily_study_time, |
| source_context=source_context, |
| ) |
| if llm_modules: |
| modules = llm_modules |
| llm_generated = True |
|
|
| |
| if llm_generated: |
| pass |
| elif course_type == "degree": |
| modules = _build_degree_modules( |
| subject_area=subject, |
| semester=extracted_semester, |
| source_context=source_context, |
| has_source=has_source, |
| ) |
| elif course_type == "playlist": |
| playlist_topics = _extract_playlist_topics(raw_request, playlist_metadata, topic or subject) |
| if playlist_topics: |
| topic = topic if topic and topic != subject else playlist_topics[0] |
| subject = subject if subject and subject != "General" else playlist_topics[0] |
| modules = _build_skill_modules( |
| topics=playlist_topics, |
| source_context=source_context, |
| has_source=has_source, |
| ) |
| elif course_type == "skill": |
| skill_topics = _extract_skill_course_topics(raw_request) or [subject] |
| modules = _build_skill_modules( |
| topics=skill_topics, |
| source_context=source_context, |
| has_source=has_source, |
| ) |
| else: |
| modules = _build_school_modules( |
| subject=subject, |
| chapter=chapter, |
| source_context=source_context, |
| has_source=has_source, |
| time_available=daily_study_time, |
| ) |
|
|
| |
| lessons_total = sum(len(m.lessons) for m in modules) |
| estimated_total = sum(m.estimated_minutes for m in modules) |
|
|
| |
| if llm_generated: |
| source_basis = "ai_generated" |
| data_source_label = ( |
| "DocDoe learning engine + your material" if has_source else "DocDoe learning engine" |
| ) |
| elif has_source: |
| source_basis = "source_upload" |
| data_source_label = "Uploaded source" |
| else: |
| source_basis = "catalog_seed" |
| data_source_label = "Starter seed" |
|
|
| |
| confidence = 0.55 if llm_generated else 0.3 |
| if subject: |
| confidence += 0.15 |
| if topic: |
| confidence += 0.15 |
| if has_source: |
| confidence += 0.2 |
| if extracted_semester or class_level: |
| confidence += 0.1 |
| if goal: |
| confidence += 0.1 |
| confidence = min(confidence, 1.0) |
|
|
| |
| trust_notes: list[str] = [] |
| if has_source_ids and not has_source: |
| trust_notes.append( |
| "Source IDs were provided, but no usable source text was loaded. " |
| "Using topic, goal, and catalog seed data only." |
| ) |
| elif not has_source: |
| trust_notes.append( |
| "No source uploaded yet. Using topic, goal, and catalog seed data only." |
| ) |
| if not extracted_semester and not class_level: |
| trust_notes.append( |
| "Class/semester level not specified. Using general-level content." |
| ) |
| if current_level: |
| trust_notes.append(f"Student self-reported level: {current_level}.") |
|
|
| prerequisites = _build_prerequisites(subject, topic) |
| key_concepts = _build_key_concepts(subject, topic, modules) |
| practice_tasks = _build_practice_tasks(subject, topic, modules) |
| quizzes = _build_quizzes(topic, modules) |
| revision_checkpoints = _build_revision_checkpoints(topic, modules) |
| student_context = _build_student_context( |
| class_level=class_level, |
| syllabus=syllabus, |
| board=board, |
| subject=subject, |
| chapter=chapter, |
| topic=topic, |
| exam_date=exam_date, |
| daily_study_time=daily_study_time, |
| time_available=time_available, |
| goal=goal, |
| ) |
| lesson_outline = _build_lesson_outline(modules) |
| prerequisite_check = _build_prerequisite_check(subject, topic, prerequisites) |
| concept_explanation = _build_concept_explanation(subject, topic, modules) |
| derivation_or_problem_steps = _build_derivation_or_problem_steps(subject, topic, modules, source_basis) |
| practice_questions = _build_practice_questions_for_tuition(topic, subject) |
| pyq_style_questions = _build_pyq_style_questions(topic, subject, has_source) |
| weak_topic_repairs = [ |
| WeakTopicRepair( |
| topic=weak_topic, |
| symptom=f"You marked {weak_topic} as weak.", |
| repair_task=f"Rebuild {weak_topic} with one definition, one example, and one practice question before continuing.", |
| ) |
| for weak_topic in weak_topics[:4] |
| if weak_topic.strip() |
| ] + _build_weak_topic_repairs(subject, topic, prerequisites) |
| revision_plan = _build_revision_plan(topic, daily_study_time, time_available, estimated_total) |
| estimated_study_time = _format_estimated_study_time(estimated_total, daily_study_time) |
|
|
| |
| if course_type == "degree": |
| final_outcome = f"Complete understanding of {subject} for semester {extracted_semester or '?'} with exam-ready knowledge" |
| elif course_type == "skill": |
| final_outcome = f"Practical {subject} skills with hands-on projects and assessment" |
| else: |
| final_outcome = f"Exam-ready command of {chapter or topic} in {subject}" |
|
|
| |
| if has_source: |
| next_action = f"Start with Module 1: {modules[0].title}" if modules else "Review the course plan" |
| else: |
| next_action = f"Upload your {subject} notes or syllabus to make this course source-aware" |
|
|
| |
| plan = CoursePlan( |
| title=f"{chapter or topic} - {subject} Tuition Path" if (chapter or topic) != subject else f"{subject} Tuition Path", |
| learner_level=class_level or degree or course_type.title(), |
| subject_area=subject, |
| source_basis=source_basis, |
| modules=modules, |
| lessons_total=lessons_total, |
| estimated_total_minutes=estimated_total, |
| prerequisites=prerequisites, |
| key_concepts=key_concepts, |
| practice_tasks=practice_tasks, |
| quizzes=quizzes, |
| revision_checkpoints=revision_checkpoints, |
| final_outcome=final_outcome, |
| next_action=next_action, |
| data_source_label=data_source_label, |
| confidence=round(confidence, 2), |
| trust_notes=trust_notes, |
| student_context=student_context, |
| lesson_outline=lesson_outline, |
| prerequisite_check=prerequisite_check, |
| concept_explanation=concept_explanation, |
| derivation_or_problem_steps=derivation_or_problem_steps, |
| practice_questions=practice_questions, |
| pyq_style_questions=pyq_style_questions, |
| weak_topic_repairs=weak_topic_repairs, |
| revision_plan=revision_plan, |
| estimated_study_time=estimated_study_time, |
| ) |
|
|
| return plan |
|
|
|
|
| def course_plan_to_output(plan: CoursePlan) -> dict[str, Any]: |
| """Convert a CoursePlan to a JSON-serializable dict.""" |
| def lesson_dict(l: Lesson) -> dict[str, Any]: |
| return { |
| "title": l.title, |
| "duration_minutes": l.duration_minutes, |
| "task": l.task, |
| "reason_type": l.reason_type, |
| "source_basis": l.source_basis, |
| "expected_output": l.expected_output, |
| "prerequisite_check": l.prerequisite_check, |
| } |
|
|
| def module_dict(m: Module) -> dict[str, Any]: |
| return { |
| "title": m.title, |
| "lessons": [lesson_dict(l) for l in m.lessons], |
| "estimated_minutes": m.estimated_minutes, |
| "source_basis": m.source_basis, |
| } |
|
|
| def quiz_dict(q: Quiz) -> dict[str, Any]: |
| return { |
| "title": q.title, |
| "question_count": q.question_count, |
| "focus_areas": q.focus_areas, |
| } |
|
|
| def practice_dict(p: PracticeTask) -> dict[str, Any]: |
| return { |
| "title": p.title, |
| "task_type": p.task_type, |
| "description": p.description, |
| "source_basis": p.source_basis, |
| } |
|
|
| def revision_dict(r: RevisionCheckpoint) -> dict[str, Any]: |
| return { |
| "title": r.title, |
| "focus_areas": r.focus_areas, |
| "estimated_minutes": r.estimated_minutes, |
| } |
|
|
| def student_context_dict(ctx: StudentContext) -> dict[str, Any]: |
| return { |
| "class_level": ctx.class_level, |
| "syllabus": ctx.syllabus, |
| "subject": ctx.subject, |
| "chapter": ctx.chapter, |
| "exam_date": ctx.exam_date, |
| "daily_study_time": ctx.daily_study_time, |
| "goal": ctx.goal, |
| } |
|
|
| def concept_dict(c: ConceptExplanation | None) -> dict[str, Any] | None: |
| if c is None: |
| return None |
| return { |
| "title": c.title, |
| "explanation": c.explanation, |
| "board_exam_focus": c.board_exam_focus, |
| } |
|
|
| def derivation_step_dict(step: DerivationProblemStep) -> dict[str, Any]: |
| return { |
| "title": step.title, |
| "steps": step.steps, |
| "common_mistake": step.common_mistake, |
| "source_basis": step.source_basis, |
| } |
|
|
| def question_dict(q: PracticeQuestion) -> dict[str, Any]: |
| return { |
| "question": q.question, |
| "marks": q.marks, |
| "answer_hint": q.answer_hint, |
| "question_type": q.question_type, |
| } |
|
|
| def repair_dict(item: WeakTopicRepair) -> dict[str, Any]: |
| return { |
| "topic": item.topic, |
| "symptom": item.symptom, |
| "repair_task": item.repair_task, |
| } |
|
|
| def revision_plan_dict(item: RevisionPlanItem) -> dict[str, Any]: |
| return { |
| "timing": item.timing, |
| "task": item.task, |
| "purpose": item.purpose, |
| "estimated_minutes": item.estimated_minutes, |
| } |
|
|
| return { |
| "title": plan.title, |
| "learner_level": plan.learner_level, |
| "subject_area": plan.subject_area, |
| "source_basis": plan.source_basis, |
| "modules": [module_dict(m) for m in plan.modules], |
| "lessons_total": plan.lessons_total, |
| "estimated_total_minutes": plan.estimated_total_minutes, |
| "prerequisites": plan.prerequisites, |
| "key_concepts": plan.key_concepts, |
| "practice_tasks": [practice_dict(p) for p in plan.practice_tasks], |
| "quizzes": [quiz_dict(q) for q in plan.quizzes], |
| "revision_checkpoints": [revision_dict(r) for r in plan.revision_checkpoints], |
| "final_outcome": plan.final_outcome, |
| "next_action": plan.next_action, |
| "data_source_label": plan.data_source_label, |
| "confidence": plan.confidence, |
| "trust_notes": plan.trust_notes, |
| "student_context": student_context_dict(plan.student_context), |
| "lesson_outline": plan.lesson_outline, |
| "prerequisite_check": plan.prerequisite_check, |
| "concept_explanation": concept_dict(plan.concept_explanation), |
| "derivation_or_problem_steps": [derivation_step_dict(step) for step in plan.derivation_or_problem_steps], |
| "practice_questions": [question_dict(q) for q in plan.practice_questions], |
| "pyq_style_questions": [question_dict(q) for q in plan.pyq_style_questions], |
| "weak_topic_repairs": [repair_dict(item) for item in plan.weak_topic_repairs], |
| "revision_plan": [revision_plan_dict(item) for item in plan.revision_plan], |
| "estimated_study_time": plan.estimated_study_time, |
| } |
|
|