·27 min

Complete Guide: Building a Personalized AI Learning System with Local LLMs, Knowledge Graphs, and Adaptive Learning

A comprehensive technical guide to building a self-hosted AI learning platform with Next.js, FastAPI, local LLMs, knowledge graphs, and retrieval-augmented generation for personalized, adaptive education.

DK

Daniel Kliewer

Author, Sovereign AI

AI Learning PlatformLocal LLMsKnowledge GraphsRAGNext.jsFastAPIPostgreSQLChromaDBAdaptive LearningPersonalized Education
Sovereign AI book cover

From the Book

This is from Sovereign AI: Building Local-First Intelligent Systems.

Get the Book — $88
Complete Guide: Building a Personalized AI Learning System with Local LLMs, Knowledge Graphs, and Adaptive Learning

Image

Github Link

Building a Personalized AI Learning System with Local LLMs

Table of Contents

1. Introduction

Why Build a Personalized AI Learning System?

Traditional e-learning platforms often rely on static content that doesn't adapt to individual learners. This guide presents a fully AI-driven personalized learning system that generates entirely new lessons for each interaction, making every session unique and context-aware.

The system dynamically adjusts content using a knowledge graph and a local LLM, ensuring learners receive increasingly relevant and challenging material based on their progress. This adaptive approach maximizes engagement and retention in ways traditional courses cannot.

Key Features

Self-Hosted & Private: Everything runs locally without reliance on cloud APIs
Dynamic Lesson Generation: Each lesson is uniquely tailored to the user's progress
Knowledge Graph-Driven: Lessons structured on connected concept maps, not linear modules
Retrieval-Augmented Generation (RAG): AI enhances lessons with relevant context
Scalable & Modular: Built with modern tech for flexibility and growth

2. System Architecture

The system uses a modular three-layer architecture:

Frontend – Next.js + React

This provides the interface where users engage with AI-generated lessons:

  • User Dashboard: Displays progress, completed lessons, and recommendations
  • Lesson UI: Renders AI-generated content in an engaging format
  • Interactive Exercises: Supports quizzes and challenges with real-time AI feedback
  • Progress Visualization: Shows topic mastery through knowledge graph visualizations
  • AI Chat: Provides on-demand explanations for concepts

Backend – FastAPI

Manages user data, lesson requests, and AI interactions:

  • Content Processing: Handles markdown files and processes them for the AI
  • Progress Tracking: Stores learning history to adapt future lessons
  • Knowledge Graph Management: Maintains concept relationships
  • API Endpoints: Connects frontend and AI layer

AI Layer – Local LLM + Knowledge Graph

The brain of the system:

  • Knowledge Graph: Maps concepts and their relationships
  • RAG Implementation: Enhances lesson quality with relevant context
  • Adaptive Generation: Creates lessons based on user progress
  • Local Execution: All AI runs on your hardware for privacy and control

Data Flow

  1. User requests a lesson from the frontend
  2. Backend queries knowledge graph and past progress
  3. AI layer generates a personalized, non-repetitive lesson
  4. Frontend displays the lesson with interactive elements
  5. User interactions update the knowledge graph and progress data

3. Tech Stack & Tools

Frontend

  • Next.js (React): For a responsive, server-rendered interface
  • TailwindCSS: For utility-first styling
  • ShadCN UI: For pre-built, customizable components
  • React-Flow: For visualizing knowledge graphs

Backend

  • FastAPI: Python-based API with async support
  • SQLAlchemy: ORM for database interactions
  • Pydantic: For data validation

Databases

  • PostgreSQL: Stores structured data (user progress, lesson history)
  • ChromaDB: Vector database for semantic search

AI Components

  • Ollama: Framework for running local LLMs
  • Mistral or Llama 3: High-quality open-source LLM
  • NetworkX: Python library for knowledge graph implementation
  • Sentence-Transformers: For generating text embeddings

4. Step-by-Step Implementation

Step 1: Environment Setup

First, let's set up our project structure and install dependencies:

bash
1# Create project directory
2mkdir ai-learning-system
3cd ai-learning-system
4
5# Create subdirectories
6mkdir -p frontend backend

Backend Setup:

bash
1cd backend
2
3# Create virtual environment
4python -m venv venv
5source venv/bin/activate # On Windows: venv\Scripts\activate
6
7# Install dependencies
8pip install fastapi uvicorn pydantic sqlalchemy psycopg2-binary chromadb sentence-transformers networkx python-multipart
9
10# Create basic directory structure
11mkdir -p app/api app/db app/models app/services

Frontend Setup:

bash
1cd ../frontend
2
3# Initialize Next.js project
4npx create-next-app@latest . --typescript --tailwind --eslint --app
5
6# Install additional dependencies
7npm install react-flow-renderer react-markdown react-dropzone

Step 2: Database Setup

PostgreSQL Setup

Let's create our database models for user progress and lesson history:

python
1# backend/app/models/database.py
2from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, Float
3from sqlalchemy.ext.declarative import declarative_base
4from sqlalchemy.orm import relationship
5import datetime
6
7Base = declarative_base()
8
9class User(Base):
10 __tablename__ = "users"
11
12 id = Column(Integer, primary_key=True, index=True)
13 username = Column(String, unique=True, index=True)
14 email = Column(String, unique=True, index=True)
15 hashed_password = Column(String)
16 created_at = Column(DateTime, default=datetime.datetime.utcnow)
17
18 progress = relationship("UserProgress", back_populates="user")
19
20class Concept(Base):
21 __tablename__ = "concepts"
22
23 id = Column(Integer, primary_key=True, index=True)
24 name = Column(String, unique=True, index=True)
25 description = Column(Text)
26 difficulty = Column(Integer) # 1-10 scale
27
28 prerequisites = relationship(
29 "ConceptRelationship",
30 primaryjoin="Concept.id==ConceptRelationship.target_id",
31 back_populates="target"
32 )
33 followups = relationship(
34 "ConceptRelationship",
35 primaryjoin="Concept.id==ConceptRelationship.source_id",
36 back_populates="source"
37 )
38
39class ConceptRelationship(Base):
40 __tablename__ = "concept_relationships"
41
42 id = Column(Integer, primary_key=True, index=True)
43 source_id = Column(Integer, ForeignKey("concepts.id"))
44 target_id = Column(Integer, ForeignKey("concepts.id"))
45 relationship_type = Column(String) # e.g., "prerequisite", "related"
46 strength = Column(Float) # 0-1 representing relationship strength
47
48 source = relationship("Concept", foreign_keys=[source_id], back_populates="followups")
49 target = relationship("Concept", foreign_keys=[target_id], back_populates="prerequisites")
50
51class UserProgress(Base):
52 __tablename__ = "user_progress"
53
54 id = Column(Integer, primary_key=True, index=True)
55 user_id = Column(Integer, ForeignKey("users.id"))
56 concept_id = Column(Integer, ForeignKey("concepts.id"))
57 mastery_level = Column(Float) # 0-1 scale
58 last_studied = Column(DateTime, default=datetime.datetime.utcnow)
59
60 user = relationship("User", back_populates="progress")
61 concept = relationship("Concept")
62
63class Lesson(Base):
64 __tablename__ = "lessons"
65
66 id = Column(Integer, primary_key=True, index=True)
67 user_id = Column(Integer, ForeignKey("users.id"))
68 concept_id = Column(Integer, ForeignKey("concepts.id"))
69 content = Column(Text)
70 generated_at = Column(DateTime, default=datetime.datetime.utcnow)
71
72 exercises = relationship("Exercise", back_populates="lesson")
73
74class Exercise(Base):
75 __tablename__ = "exercises"
76
77 id = Column(Integer, primary_key=True, index=True)
78 lesson_id = Column(Integer, ForeignKey("lessons.id"))
79 question = Column(Text)
80 answer = Column(Text)
81
82 lesson = relationship("Lesson", back_populates="exercises")

Now, let's set up the database connection:

python
1# backend/app/db/database.py
2from sqlalchemy import create_engine
3from sqlalchemy.ext.declarative import declarative_base
4from sqlalchemy.orm import sessionmaker
5import os
6from dotenv import load_dotenv
7
8load_dotenv()
9
10DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://username:password@localhost/ai_learning")
11
12engine = create_engine(DATABASE_URL)
13SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
14
15def get_db():
16 db = SessionLocal()
17 try:
18 yield db
19 finally:
20 db.close()

ChromaDB Setup

python
1# backend/app/db/vector_store.py
2import chromadb
3from chromadb.config import Settings
4import os
5
6class VectorStore:
7 def __init__(self, persist_directory="./chroma_data"):
8 self.client = chromadb.Client(Settings(
9 chroma_db_impl="duckdb+parquet",
10 persist_directory=persist_directory
11 ))
12
13 # Create collections if they don't exist
14 self.lesson_collection = self.client.get_or_create_collection("lessons")
15 self.concept_collection = self.client.get_or_create_collection("concepts")
16
17 def add_concept(self, concept_id, concept_name, concept_description, embedding):
18 """Add a concept to the vector store"""
19 self.concept_collection.add(
20 ids=[str(concept_id)],
21 embeddings=[embedding],
22 metadatas=[{"name": concept_name}],
23 documents=[concept_description]
24 )
25
26 def add_lesson_chunk(self, chunk_id, lesson_id, concept_id, content, embedding):
27 """Add a lesson chunk to the vector store"""
28 self.lesson_collection.add(
29 ids=[str(chunk_id)],
30 embeddings=[embedding],
31 metadatas=[{"lesson_id": str(lesson_id), "concept_id": str(concept_id)}],
32 documents=[content]
33 )
34
35 def search_similar_concepts(self, query_embedding, n_results=5):
36 """Find similar concepts based on embedding"""
37 results = self.concept_collection.query(
38 query_embeddings=[query_embedding],
39 n_results=n_results
40 )
41 return results
42
43 def search_relevant_content(self, query_embedding, n_results=10):
44 """Find relevant lesson content based on embedding"""
45 results = self.lesson_collection.query(
46 query_embeddings=[query_embedding],
47 n_results=n_results
48 )
49 return results
50
51# Singleton instance to be used throughout the app
52vector_store = VectorStore()

Step 3: Knowledge Graph Implementation

Let's implement the knowledge graph using NetworkX:

python
1# backend/app/services/knowledge_graph.py
2import networkx as nx
3from app.db.database import get_db
4from app.models.database import Concept, ConceptRelationship, UserProgress
5import json
6
7class KnowledgeGraph:
8 def __init__(self):
9 self.graph = nx.DiGraph()
10 self.load_from_database()
11
12 def load_from_database(self):
13 """Load concept relationships from database into NetworkX graph"""
14 db = next(get_db())
15
16 # Get all concepts
17 concepts = db.query(Concept).all()
18 for concept in concepts:
19 self.graph.add_node(
20 concept.id,
21 name=concept.name,
22 description=concept.description,
23 difficulty=concept.difficulty
24 )
25
26 # Get all relationships
27 relationships = db.query(ConceptRelationship).all()
28 for rel in relationships:
29 self.graph.add_edge(
30 rel.source_id,
31 rel.target_id,
32 type=rel.relationship_type,
33 strength=rel.strength
34 )
35
36 def get_prerequisites(self, concept_id):
37 """Get prerequisites for a given concept"""
38 if not self.graph.has_node(concept_id):
39 return []
40
41 prerequisites = []
42 for pred in self.graph.predecessors(concept_id):
43 if self.graph[pred][concept_id].get('type') == 'prerequisite':
44 prerequisites.append(pred)
45
46 return prerequisites
47
48 def get_next_concepts(self, concept_id):
49 """Get concepts that follow the current one"""
50 if not self.graph.has_node(concept_id):
51 return []
52
53 next_concepts = []
54 for succ in self.graph.successors(concept_id):
55 next_concepts.append(succ)
56
57 return next_concepts
58
59 def get_learning_path(self, start_concept, target_concept):
60 """Find shortest path between concepts"""
61 if not (self.graph.has_node(start_concept) and self.graph.has_node(target_concept)):
62 return []
63
64 try:
65 path = nx.shortest_path(self.graph, start_concept, target_concept)
66 return path
67 except nx.NetworkXNoPath:
68 return []
69
70 def recommend_next_concept(self, user_id):
71 """Recommend next concept for user based on progress"""
72 db = next(get_db())
73
74 # Get user's current progress
75 progress_records = db.query(UserProgress).filter(
76 UserProgress.user_id == user_id
77 ).all()
78
79 # Create a dict of concept_id -> mastery_level
80 mastery = {p.concept_id: p.mastery_level for p in progress_records}
81
82 # Find concepts user has started but not mastered
83 in_progress = [cid for cid, level in mastery.items() if level < 0.8]
84
85 if in_progress:
86 # Return the concept with lowest mastery
87 return min(in_progress, key=lambda x: mastery.get(x, 0))
88
89 # If no concepts in progress, find new concepts where prerequisites are met
90 mastered = [cid for cid, level in mastery.items() if level >= 0.8]
91
92 candidate_concepts = []
93 for concept_id in self.graph.nodes:
94 if concept_id in mastery:
95 continue # Skip concepts user has already started
96
97 prereqs = self.get_prerequisites(concept_id)
98 if not prereqs or all(p in mastered for p in prereqs):
99 # All prerequisites met
100 candidate_concepts.append(concept_id)
101
102 if not candidate_concepts:
103 # If no obvious next concepts, recommend any starter concept
104 starter_concepts = [n for n in self.graph.nodes if not list(self.graph.predecessors(n))]
105 return starter_concepts[0] if starter_concepts else list(self.graph.nodes)[0]
106
107 # Return easiest candidate concept (by difficulty)
108 return min(candidate_concepts, key=lambda x: self.graph.nodes[x].get('difficulty', 5))
109
110# Create singleton instance
111knowledge_graph = KnowledgeGraph()
112
113# Ensure graph is updated when DB changes
114def refresh_knowledge_graph():
115 knowledge_graph.load_from_database()

Step 4: Embedding Service

Let's create a service for generating embeddings:

python
1# backend/app/services/embedding_service.py
2from sentence_transformers import SentenceTransformer
3import numpy as np
4
5class EmbeddingService:
6 def __init__(self, model_name="all-MiniLM-L6-v2"):
7 self.model = SentenceTransformer(model_name)
8
9 def get_embedding(self, text):
10 """Generate embedding for text"""
11 return self.model.encode(text).tolist()
12
13 def get_embeddings(self, texts):
14 """Generate embeddings for multiple texts"""
15 return self.model.encode(texts).tolist()
16
17# Create singleton instance
18embedding_service = EmbeddingService()

Step 5: LLM Service with Ollama

python
1# backend/app/services/llm_service.py
2import requests
3import json
4import os
5from dotenv import load_dotenv
6
7load_dotenv()
8
9OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
10MODEL_NAME = os.getenv("LLM_MODEL", "mistral")
11
12class LLMService:
13 def __init__(self, base_url=OLLAMA_BASE_URL, model=MODEL_NAME):
14 self.base_url = base_url
15 self.model = model
16 self.generate_url = f"{self.base_url}/api/generate"
17
18 def generate_text(self, prompt, max_tokens=2000, temperature=0.7):
19 """Generate text from prompt using Ollama API"""
20 payload = {
21 "model": self.model,
22 "prompt": prompt,
23 "stream": False,
24 "options": {
25 "temperature": temperature,
26 "max_tokens": max_tokens
27 }
28 }
29
30 try:
31 response = requests.post(self.generate_url, json=payload)
32 response.raise_for_status()
33 result = response.json()
34 return result.get("response", "")
35 except requests.exceptions.RequestException as e:
36 print(f"Error calling Ollama API: {e}")
37 return f"Error: {str(e)}"
38
39 def generate_lesson(self, concept_name, concept_description, user_level="beginner",
40 previous_knowledge=None, related_content=None):
41 """Generate a complete lesson with RAG enhancement"""
42 # Build context from related content
43 context = ""
44 if related_content:
45 context = "Related information:\n" + "\n".join(related_content)
46
47 # Include previous knowledge if available
48 previous = ""
49 if previous_knowledge:
50 previous = "The user has previously learned:\n" + "\n".join(previous_knowledge)
51
52 prompt = f"""
53 You are an expert tutor creating a lesson about "{concept_name}".
54
55 Basic description of the concept: {concept_description}
56
57 User knowledge level: {user_level}
58
59 {previous}
60
61 {context}
62
63 Create a comprehensive lesson that includes:
64 1. A clear explanation of {concept_name}
65 2. Key points to understand
66 3. 2-3 concrete examples that demonstrate the concept
67 4. 3 practice exercises with answer explanations
68 5. A summary of what was covered
69
70 Format the lesson using markdown with proper headings, lists, and code blocks if needed.
71 Tailor the difficulty to {user_level} level while ensuring the content is engaging and not repetitive.
72 """
73
74 return self.generate_text(prompt)
75
76 def generate_exercise_feedback(self, exercise, user_answer, correct_answer):
77 """Generate feedback on a user's exercise answer"""
78 prompt = f"""
79 Exercise: {exercise}
80
81 User's answer: {user_answer}
82
83 Correct answer: {correct_answer}
84
85 Provide helpful feedback on the user's answer. Include:
86 1. Whether the answer is correct, partially correct, or incorrect
87 2. Explanation of any mistakes or misconceptions
88 3. Guidance on how to improve their understanding
89 4. Positive reinforcement for what they did correctly
90
91 Keep your tone encouraging and constructive.
92 """
93
94 return self.generate_text(prompt, max_tokens=800, temperature=0.5)
95
96# Create singleton instance
97llm_service = LLMService()

Step 6: Backend API

Let's implement the main FastAPI application:

python
1# backend/app/main.py
2from fastapi import FastAPI, Depends, HTTPException, UploadFile, File, Form
3from fastapi.middleware.cors import CORSMiddleware
4from sqlalchemy.orm import Session
5import os
6import json
7from typing import List, Optional
8from pydantic import BaseModel
9
10from app.db.database import get_db, engine
11from app.models.database import Base, User, Concept, UserProgress, Lesson, Exercise
12from app.db.vector_store import vector_store
13from app.services.embedding_service import embedding_service
14from app.services.knowledge_graph import knowledge_graph, refresh_knowledge_graph
15from app.services.llm_service import llm_service
16
17# Create database tables
18Base.metadata.create_all(bind=engine)
19
20app = FastAPI(title="AI Learning System API")
21
22# Configure CORS
23app.add_middleware(
24 CORSMiddleware,
25 allow_origins=["http://localhost:3000"], # Frontend URL
26 allow_credentials=True,
27 allow_methods=["*"],
28 allow_headers=["*"],
29)
30
31# Pydantic models for API
32class ConceptBase(BaseModel):
33 name: str
34 description: str
35 difficulty: int
36
37class ConceptCreate(ConceptBase):
38 pass
39
40class ConceptRead(ConceptBase):
41 id: int
42
43 class Config:
44 orm_mode = True
45
46class LessonRequest(BaseModel):
47 concept_id: Optional[int] = None
48 user_id: int
49
50class LessonResponse(BaseModel):
51 id: int
52 content: str
53 concept: ConceptRead
54 exercises: List[dict]
55
56 class Config:
57 orm_mode = True
58
59# Routes
60@app.get("/")
61def read_root():
62 return {"message": "AI Learning System API"}
63
64@app.post("/concepts/", response_model=ConceptRead)
65def create_concept(concept: ConceptCreate, db: Session = Depends(get_db)):
66 db_concept = Concept(
67 name=concept.name,
68 description=concept.description,
69 difficulty=concept.difficulty
70 )
71 db.add(db_concept)
72 db.commit()
73 db.refresh(db_concept)
74
75 # Add to vector store
76 embedding = embedding_service.get_embedding(f"{concept.name} {concept.description}")
77 vector_store.add_concept(
78 db_concept.id,
79 db_concept.name,
80 db_concept.description,
81 embedding
82 )
83
84 # Refresh knowledge graph
85 refresh_knowledge_graph()
86
87 return db_concept
88
89@app.get("/concepts/", response_model=List[ConceptRead])
90def get_concepts(db: Session = Depends(get_db)):
91 concepts = db.query(Concept).all()
92 return concepts
93
94@app.post("/lessons/generate/", response_model=dict)
95def generate_lesson(request: LessonRequest, db: Session = Depends(get_db)):
96 # Check if user exists
97 user = db.query(User).filter(User.id == request.user_id).first()
98 if not user:
99 raise HTTPException(status_code=404, detail="User not found")
100
101 # Determine which concept to teach
102 concept_id = request.concept_id
103 if not concept_id:
104 # Use knowledge graph to recommend next concept
105 concept_id = knowledge_graph.recommend_next_concept(request.user_id)
106
107 concept = db.query(Concept).filter(Concept.id == concept_id).first()
108 if not concept:
109 raise HTTPException(status_code=404, detail="Concept not found")
110
111 # Get user's level based on progress
112 progress = db.query(UserProgress).filter(
113 UserProgress.user_id == request.user_id,
114 UserProgress.concept_id == concept_id
115 ).first()
116
117 user_level = "beginner"
118 if progress:
119 if progress.mastery_level > 0.8:
120 user_level = "advanced"
121 elif progress.mastery_level > 0.4:
122 user_level = "intermediate"
123
124 # Get previous knowledge (mastered concepts)
125 mastered_concepts = db.query(Concept).join(UserProgress).filter(
126 UserProgress.user_id == request.user_id,
127 UserProgress.mastery_level >= 0.8
128 ).all()
129
130 previous_knowledge = [f"{c.name}: {c.description}" for c in mastered_concepts]
131
132 # Find related content using RAG
133 concept_embedding = embedding_service.get_embedding(
134 f"{concept.name} {concept.description}"
135 )
136
137 related_results = vector_store.search_relevant_content(concept_embedding)
138 related_content = related_results.get("documents", [])
139
140 # Generate lesson with LLM
141 lesson_content = llm_service.generate_lesson(
142 concept.name,
143 concept.description,
144 user_level,
145 previous_knowledge,
146 related_content
147 )
148
149 # Parse exercises from the lesson (simplified)
150 # In a real implementation, you'd use a more robust method to extract exercises
151 exercises = []
152
153 # Create lesson record
154 db_lesson = Lesson(
155 user_id=request.user_id,
156 concept_id=concept_id,
157 content=lesson_content
158 )
159 db.add(db_lesson)
160 db.commit()
161 db.refresh(db_lesson)
162
163 # Update or create user progress
164 if not progress:
165 progress = UserProgress(
166 user_id=request.user_id,
167 concept_id=concept_id,
168 mastery_level=0.1 # Initial mastery
169 )
170 db.add(progress)
171 else:
172 # Increment slightly just for viewing the lesson
173 progress.mastery_level = min(progress.mastery_level + 0.05, 1.0)
174
175 db.commit()
176
177 # Return lesson data
178 return {
179 "id": db_lesson.id,
180 "content": lesson_content,
181 "concept": {
182 "id": concept.id,
183 "name": concept.name,
184 "description": concept.description,
185 "difficulty": concept.difficulty
186 },
187 "exercises": exercises
188 }
189
190@app.post("/upload/")
191async def upload_markdown(
192 file: UploadFile = File(...),
193 user_id: int = Form(...)
194):
195 # Read file contents
196 contents = await file.read()
197 text = contents.decode("utf-8")
198
199 # Here you would implement markdown parsing to extract concepts
200 # For simplicity, we'll just assume the file contains concept data
201
202 # Example implementation:
203 import re
204
205 # Extract headings as concepts
206 headings = re.findall(r'## (.*?)\n', text)
207
208 # Process each heading as a concept
209 for heading in headings:
210 # Extract paragraph after heading as description
211 description_match = re.search(f'## {re.escape(heading)}\n\n(.*?)\n\n', text, re.DOTALL)
212 description = description_match.group(1) if description_match else "No description available"
213
214 # Create concept
215 db = next(get_db())
216 concept = Concept(
217 name=heading,
218 description=description,
219 difficulty=5 # Default difficulty
220 )
221 db.add(concept)
222 db.commit()
223 db.refresh(concept)
224
225 # Add to vector store
226 embedding = embedding_service.get_embedding(f"{heading} {description}")
227 vector_store.add_concept(
228 concept.id,
229 concept.name,
230 concept.description,
231 embedding
232 )
233
234 # Refresh knowledge graph
235 refresh_knowledge_graph()
236
237 return {"message": f"Processed {len(headings)} concepts from {file.filename}"}
238
239@app.post("/progress/update/")
240def update_progress(
241 user_id: int,
242 concept_id: int,
243 mastery_level: float,
244 db: Session = Depends(get_db)
245):
246 progress = db.query(UserProgress).filter(
247 UserProgress.user_id == user_id,
248 UserProgress.concept_id == concept_id
249 ).first()
250
251 if not progress:
252 progress = UserProgress(
253 user_id=user_id,
254 concept_id=concept_id,
255 mastery_level=mastery_level
256 )
257 db.add(progress)
258 else:
259 progress.mastery_level = mastery_level
260
261 db.commit()
262 return {"status": "success"}
263
264if __name__ == "__main__":
265 import uvicorn
266 uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)

Step 7: Frontend Implementation

Let's create the key components for our Next.js frontend:

App Layout

tsx
1// frontend/app/layout.tsx
2import './globals.css'
3import type { Metadata } from 'next'
4import { Inter } from 'next/font/google'
5import Sidebar from '@/components/Sidebar'
6
7const inter = Inter({ subsets: ['latin'] })
8
9export const metadata: Metadata = {
10 title: 'AI Learning System',
11 description: 'Personalized learning powered by AI',
12}
13
14export default function RootLayout({
15 children,
16}: {
17 children: React.ReactNode
18}) {
19 return (
20 <html lang="en">
21 <body className={inter.className}>
22 <div className="flex h-screen">
23 <Sidebar />
24 <main className="flex-1 p-6 overflow-auto">
25 {children}
26 </main>
27 </div>
28 </body>
29 </html>
30 )
31}

Sidebar Component

tsx
1// frontend/components/Sidebar.tsx
2import Link from 'next/link'
3import { usePathname } from 'next/navigation'
4
5const Sidebar = () => {
6 const pathname = usePathname()
7
8 const links = [
9 { href: '/', label: 'Dashboard' },
10 { href: '/learn', label: 'Learn' },
11 { href: '/progress', label: 'Progress' },
12 { href: '/upload', label: 'Upload Content' },
13 ]
14
15 return (
16 <aside className="w-64 bg-gray-800 text-white p-4">
17 <h1 className="text-xl font-bold mb-6">AI Learning System</h1>
18 <nav>
19 <ul>
20 {links.map((link) => (
21 <li key={link.href} className="mb-2">
22 <Link href={link.href}
23 className={`block p-2 rounded hover:bg-gray-700 ${
24 pathname === link.href ? 'bg-gray-700' : ''
25 }`}
26 >
27 {link.label}
28 </Link>
29 </li>
30 ))}
31 </ul>
32 </nav>
33 </aside>
34 )
35}
36
37export default Sidebar

Dashboard Page

tsx
1// frontend/app/page.tsx
2'use client'
3
4import { useEffect, useState } from 'react'
5import Link from 'next/link'
6
7export default function Dashboard() {
8 const [recentLessons, setRecentLessons] = useState([])
9 const [recommendations, setRecommendations] = useState([])
10 const [loading, setLoading] = useState(true)
11
12 // In a real app, you'd fetch this data from your API
13 useEffect(() => {
14 // Mock data for demonstration
15 setRecentLessons([
16 { id: 1, title: 'Introduction to Neural Networks', date: '2023-05-15' },
17 { id: 2, title: 'Python Basics', date: '2023-05-12' },
18 ])
19
20 setRecommendations([
21 { id: 3, title: 'Reinforcement Learning', difficulty: 'Intermediate' },
22 { id: 4, title: 'Data Preprocessing', difficulty: 'Beginner' },
23 ])
24
25 setLoading(false)
26 }, [])
27
28 if (loading) {
29 return <div className="flex justify-center items-center h-full">Loading...</div>
30 }
31
32 return (
33 <div>
34 <h1 className="text-3xl font-bold mb-6">Learning Dashboard</h1>
35
36 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
37 <div className="bg-white p-6 rounded-lg shadow">
38 <h2 className="text-xl font-semibold mb-4">Recent Lessons</h2>
39 {recentLessons.length > 0 ? (
40 <ul className="space-y-2">
41 {recentLessons.map((lesson) => (
42 <li key={lesson.id} className="border-b pb-2">
43 <Link href={`/learn/${lesson.id}`} className="text-blue-600 hover:underline">
44 {lesson.title}
45 </Link>
46 <p className="text-sm text-gray-500">{lesson.date}</p>
47 </li>
48 ))}
49 </ul>
50 ) : (
51 <p>No recent lessons found.</p>
52 )}
53 </div>
54
55 <div className="bg-white p-6 rounded-lg shadow">
56 <h2 className="text-xl font-semibold mb-4">Recommended For You</h2>
57 {recommendations.length > 0 ? (
58 <ul className="space-y-2">
59 {recommendations.map((rec) => (
60 <li key={rec.id} className="border-b pb-2">
61 <Link href={`/learn/start?concept=${rec.id}`} className="text-blue-600 hover:underline">
62 {rec.title}
63 </Link>
64 <p className="text-sm text-gray-500">Difficulty: {rec.difficulty}</p>
65 </li>
66 ))}
67 </ul>
68 ) : (
69 <p>No recommendations available.</p>
70 )}
71 </div>
72 </div>
73
74 <div className="mt-6 bg-white p-6 rounded-lg shadow">
75 <h2 className="text-xl font-semibold mb-4">Your Learning Progress</h2>
76 <div className="h-64 flex items-center justify-center border border-gray-200 rounded">
77 <p className="text-gray-500">Progress visualization will appear here</p>
78 </div>
79 </div>
80 </div>
81 )
82}

Learn Page

tsx
1// frontend/app/learn/page.tsx
2'use client'
3
4import { useState } from 'react'
5import { useRouter } from 'next/navigation'
6
7export default function LearnPage() {
8 const router = useRouter()
9 const [loading, setLoading] = useState(false)
10
11 // Mock data - in a real app you'd fetch these from your API
12 const topics = [
13 { id: 1, name: 'Python Basics', difficulty: 'Beginner' },
14 { id: 2, name: 'Data Structures', difficulty: 'Intermediate' },
15 { id: 3, name: 'Machine Learning Fundamentals', difficulty: 'Intermediate' },
16 { id: 4, name: 'Neural Networks', difficulty: 'Advanced' },
17 ]
18
19 const startLesson = async (topicId: number) => {
20 setLoading(true)
21
22 try {
23 // In a real app, you'd make an API call to generate a lesson
24 // const response = await fetch('/api/lessons/generate', {
25 // method: 'POST',
26 // headers: { 'Content-Type': 'application/json' },
27 // body: JSON.stringify({ concept_id: topicId, user_id: 1 }),
28 // })
29 // const data = await response.json()
30
31 // For demo purposes, we'll just navigate to a mock lesson
32 router.push(`/learn/${topicId}`)
33 } catch (error) {
34 console.error('Error starting lesson:', error)
35 } finally {
36 setLoading(false)
37 }
38 }
39
40 const getRecommendedLesson = async () => {
41 setLoading(true)
42
43 try {
44 // In a real app, you'd make an API call to get a recommended lesson
45 // const response = await fetch('/api/lessons/recommend', {
46 // method: 'POST',
47 // headers: { 'Content-Type': 'application/json' },
48 // body: JSON.stringify({ user_id: 1 }),
49 // })
50 // const data = await response.json()
51
52 // For demo, we'll randomly select a topic
53 const randomTopic = topics[Math.floor(Math.random() * topics.length)]
54 router.push(`/learn/${randomTopic.id}`)
55 } catch (error) {
56 console.error('Error getting recommendation:', error)
57 } finally {
58 setLoading(false)
59 }
60 }
61
62 return (
63 <div>
64 <h1 className="text-3xl font-bold mb-6">Start Learning</h1>
65
66 <div className="mb-6">
67 <button
68 onClick={getRecommendedLesson}
69 disabled={loading}
70 className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
71 >
72 {loading ? 'Loading...' : 'Generate Recommended Lesson'}
73 </button>
74 </div>
75
76 <div className="bg-white p-6 rounded-lg shadow">
77 <h2 className="text-xl font-semibold mb-4">Available Topics</h2>
78 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
79 {topics.map((topic) => (
80 <div key={topic.id} className="border rounded p-4">
81 <h3 className="font-medium">{topic.name}</h3>
82 <p className="text-sm text-gray-500 mb-3">Difficulty: {topic.difficulty}</p>
83 <button
84 onClick={() => startLesson(topic.id)}
85 disabled={loading}
86 className="px-3 py-1 bg-blue-600 text-white text-sm rounded hover:bg-blue-700 disabled:opacity-50"
87 >
88 Start Lesson
89 </button>
90 </div>
91 ))}
92 </div>
93 </div>
94 </div>
95 )
96}

Lesson Display Component

tsx
1// frontend/app/learn/[id]/page.tsx
2'use client'
3
4import { useState, useEffect } from 'react'
5import ReactMarkdown from 'react-markdown'
6
7export default function LessonPage({ params }: { params: { id: string } }) {
8 const [lesson, setLesson] = useState<any>(null)
9 const [loading, setLoading] = useState(true)
10 const [activeTab, setActiveTab] = useState('lesson')
11 const [userAnswers, setUserAnswers] = useState<Record<number, string>>({})
12 const [feedback, setFeedback] = useState<Record<number, string>>({})
13
14 useEffect(() => {
15 // In a real app, fetch from your API
16 // For demo, we'll use mock data
17 const mockLesson = {
18 id: parseInt(params.id),
19 title: 'Introduction to Neural Networks',
20 content: `
21# Introduction to Neural Networks
22
23Neural networks are computational models inspired by the human brain. They consist of layers of interconnected nodes or "neurons" that process information.
24
25## Key Concepts
26
27- **Neurons**: Basic units that receive inputs, apply weights, and output signals
28- **Layers**: Groups of neurons that process information sequentially
29- **Activation Functions**: Functions that determine the output of a neuron
30- **Weights and Biases**: Parameters that are adjusted during training
31
32## How Neural Networks Work
33
34A neural network processes data through layers:
35
361. Input layer receives the initial data
372. Hidden layers process the data
383. Output layer produces the final result
39
40## Examples
41
42### Example 1: Image Recognition
43
44A neural network can be trained to recognize images:
45- Input: Pixel values from an image
46- Process: Multiple layers extract features (edges, shapes, etc.)
47- Output: Classification (e.g., "cat", "dog", "car")
48
49### Example 2: Natural Language Processing
50
51Neural networks power modern language models:
52- Input: Text converted to numerical representations
53- Process: Layers extract meaning and context
54- Output: Generated text, translations, or classifications
55 `,
56 exercises: [
57 {
58 id: 1,
59 question: "What is the main inspiration for neural networks?",
60 answer: "The human brain"
61 },
62 {
63 id: 2,
64 question: "Name the three main types of layers in a neural network.",
65 answer: "Input layer, hidden layers, and output layer"
66 },
67 {
68 id: 3,
69 question: "What parameters are adjusted during the training process?",
70 answer: "Weights and biases"
71 }
72 ]
73 }
74
75 setLesson(mockLesson)
76 setLoading(false)
77 }, [params.id])
78
79 const submitAnswer = async (exerciseId: number) => {
80 const userAnswer = userAnswers[exerciseId] || ''
81
82 // In a real app, you'd send this to your API for evaluation
83 // For demo, we'll just compare with the expected answer
84 const exercise = lesson.exercises.find((ex: any) => ex.id === exerciseId)
85
86 if (exercise) {
87 const correctAnswer = exercise.answer
88
89 // Simple check - in a real app, use the LLM to evaluate
90 let feedbackText
91 if (userAnswer.toLowerCase() === correctAnswer.toLowerCase()) {
92 feedbackText = "Correct! Well done."
93 } else {
94 feedbackText = `Not quite. The correct answer is: ${correctAnswer}`
95 }
96
97 setFeedback({
98 ...feedback,
99 [exerciseId]: feedbackText
100 })
101 }
102 }
103
104 if (loading) {
105 return <div className="flex justify-center items-center h-full">Loading lesson...</div>
106 }
107
108 return (
109 <div>
110 <h1 className="text-3xl font-bold mb-6">{lesson.title}</h1>
111
112 <div className="mb-6">
113 <nav className="flex border-b">
114 <button
115 className={`py-2 px-4 ${activeTab === 'lesson' ? 'border-b-2 border-blue-500 font-medium' : ''}`}
116 onClick={() => setActiveTab('lesson')}
117 >
118 Lesson
119 </button>
120 <button
121 className={`py-2 px-4 ${activeTab === 'exercises' ? 'border-b-2 border-blue-500 font-medium' : ''}`}
122 onClick={() => setActiveTab('exercises')}
123 >
124 Exercises
125 </button>
126 </nav>
127 </div>
128
129 <div className="bg-white p-6 rounded-lg shadow">
130 {activeTab === 'lesson' ? (
131 <div className="prose max-w-none">
132 <ReactMarkdown>{lesson.content}</ReactMarkdown>
133 </div>
134 ) : (
135 <div>
136 <h2 className="text-xl font-semibold mb-4">Practice Exercises</h2>
137 <div className="space-y-6">
138 {lesson.exercises.map((exercise: any) => (
139 <div key={exercise.id} className="border p-4 rounded">
140 <p className="font-medium mb-2">{exercise.question}</p>
141 <textarea
142 className="w-full border rounded p-2 mb-2"
143 placeholder="Your answer..."
144 rows={3}
145 value={userAnswers[exercise.id] || ''}
146 onChange={(e) => setUserAnswers({
147 ...userAnswers,
148 [exercise.id]: e.target.value
149 })}
150 />
151 <button
152 className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"
153 onClick={() => submitAnswer(exercise.id)}
154 >
155 Submit Answer
156 </button>
157
158 {feedback[exercise.id] && (
159 <div className={`mt-2 p-2 rounded ${
160 feedback[exercise.id].startsWith('Correct')
161 ? 'bg-green-100 text-green-800'
162 : 'bg-red-100 text-red-800'
163 }`}>
164 {feedback[exercise.id]}
165 </div>
166 )}
167 </div>
168 ))}
169 </div>
170 </div>
171 )}
172 </div>
173
174 <div className="mt-6 flex justify-between">
175 <button className="px-4 py-2 bg-gray-600 text-white rounded hover:bg-gray-700">
176 Previous Lesson
177 </button>
178 <button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
179 Mark as Complete
180 </button>
181 </div>
182 </div>
183 )
184}

File Upload Component

tsx
1// frontend/app/upload/page.tsx
2'use client'
3
4import { useCallback, useState } from 'react'
5import { useDropzone } from 'react-dropzone'
6
7export default function UploadPage() {
8 const [uploading, setUploading] = useState(false)
9 const [uploadStatus, setUploadStatus] = useState<null | { success: boolean; message: string }>(null)
10
11 const onDrop = useCallback(async (acceptedFiles: File[]) => {
12 if (acceptedFiles.length === 0) return
13
14 setUploading(true)
15 setUploadStatus(null)
16
17 try {
18 const file = acceptedFiles[0]
19
20 // Create form data for file upload
21 const formData = new FormData()
22 formData.append('file', file)
23 formData.append('user_id', '1') // In a real app, get from auth context
24
25 // In a real app, send to your API
26 // const response = await fetch('http://localhost:8000/upload/', {
27 // method: 'POST',
28 // body: formData,
29 // })
30
31 // For demo purposes, simulate success
32 await new Promise(resolve => setTimeout(resolve, 1500))
33
34 setUploadStatus({
35 success: true,
36 message: `Successfully processed ${file.name}`
37 })
38 } catch (error) {
39 console.error('Upload error:', error)
40 setUploadStatus({
41 success: false,
42 message: 'Error uploading file. Please try again.'
43 })
44 } finally {
45 setUploading(false)
46 }
47 }, [])
48
49 const { getRootProps, getInputProps, isDragActive } = useDropzone({
50 onDrop,
51 accept: {
52 'text/markdown': ['.md'],
53 'text/plain': ['.txt']
54 },
55 maxFiles: 1
56 })
57
58 return (
59 <div>
60 <h1 className="text-3xl font-bold mb-6">Upload Learning Material</h1>
61
62 <div className="bg-white p-6 rounded-lg shadow">
63 <p className="mb-4">
64 Upload markdown files containing learning materials. The system will process the content
65 and extract concepts, examples, and exercises.
66 </p>
67
68 <div
69 {...getRootProps()}
70 className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer ${
71 isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
72 }`}
73 >
74 <input {...getInputProps()} />
75 {uploading ? (
76 <p>Uploading...</p>
77 ) : isDragActive ? (
78 <p>Drop the file here...</p>
79 ) : (
80 <div>
81 <p>Drag and drop a markdown file here, or click to select a file</p>
82 <p className="text-sm text-gray-500 mt-2">Supports .md and .txt files</p>
83 </div>
84 )}
85 </div>
86
87 {uploadStatus && (
88 <div
89 className={`mt-4 p-3 rounded ${
90 uploadStatus.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
91 }`}
92 >
93 {uploadStatus.message}
94 </div>
95 )}
96
97 <div className="mt-6">
98 <h3 className="font-medium mb-2">How it works:</h3>
99 <ol className="list-decimal list-inside space-y-1 text-sm">
100 <li>Upload a markdown file with headings and content</li>
101 <li>The system extracts concepts and their relationships</li>
102 <li>Content is processed into the knowledge graph</li>
103 <li>AI generates personalized lessons based on this content</li>
104 </ol>
105 </div>
106 </div>
107 </div>
108 )
109}

Step 8: Bringing It Together

Now you need to make both systems work together:

  1. Start the backend service:
bash
1cd backend
2python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
  1. In a separate terminal, start the frontend:
bash
1cd frontend
2npm run dev
  1. Ensure Ollama is running with your chosen model:
bash
1ollama run mistral

5. Optimization & Expansion

Explain-Back Challenges

One of the most effective ways to enhance learning is to have students explain concepts back in their own words. Let's implement this:

python
1# backend/app/services/llm_service.py
2# Add this method to the LLMService class
3
4def evaluate_explanation(self, concept_name, concept_description, user_explanation):
5 """Evaluate a user's explanation of a concept"""
6 prompt = f"""
7 Concept: {concept_name}
8
9 Expert explanation: {concept_description}
10
11 User explanation: {user_explanation}
12
13 As an AI tutor, evaluate how well the user has understood and explained the concept.
14
15 Provide feedback in the following format:
16 - Accuracy (0-10): [score]
17 - Completeness (0-10): [score]
18 - Areas of strength: [what they explained well]
19 - Areas for improvement: [what they missed or misunderstood]
20 - Suggestions: [specific advice to improve understanding]
21
22 Keep your tone encouraging and constructive.
23 """
24
25 return self.generate_text(prompt, max_tokens=800, temperature=0.3)

Adaptive Scaling

To implement adaptive difficulty scaling:

python
1# backend/app/services/knowledge_graph.py
2# Add this method to KnowledgeGraph class
3
4def get_appropriate_difficulty(self, user_id, max_difficulty=10):
5 """Determine appropriate difficulty level based on user mastery"""
6 db = next(get_db())
7
8 # Get average mastery level across all concepts
9 progress = db.query(UserProgress).filter(
10 UserProgress.user_id == user_id
11 ).all()
12
13 if not progress:
14 return 2 # Start with easy concepts for new users
15
16 avg_mastery = sum(p.mastery_level for p in progress) / len(progress)
17
18 # Scale difficulty based on mastery (higher mastery = higher difficulty)
19 # This is a simple linear scaling approach
20 recommended_difficulty = int(avg_mastery * 10) + 1
21
22 # Cap at max_difficulty
23 return min(recommended_difficulty, max_difficulty)

Custom Learning Paths

Allow users to define their own learning goals:

python
1# backend/app/main.py
2# Add this endpoint
3
4@app.post("/learning-path/create/")
5def create_learning_path(
6 user_id: int,
7 goal_concept_id: int,
8 db: Session = Depends(get_db)
9):
10 # Verify user and concept exist
11 user = db.query(User).filter(User.id == user_id).first()
12 goal_concept = db.query(Concept).filter(Concept.id == goal_concept_id).first()
13
14 if not user or not goal_concept:
15 raise HTTPException(status_code=404, detail="User or concept not found")
16
17 # Get user's current knowledge state
18 mastered_concepts = db.query(Concept).join(UserProgress).filter(
19 UserProgress.user_id == user_id,
20 UserProgress.mastery_level >= 0.8
21 ).all()
22
23 # If user has no mastered concepts, find starter concepts
24 if not mastered_concepts:
25 starter_concepts = []
26 for node in knowledge_graph.graph.nodes:
27 if not list(knowledge_graph.graph.predecessors(node)):
28 # This is a starter node with no prerequisites
29 starter_concepts.append(node)
30
31 return {
32 "starter_concepts": [
33 {"id": c, "name": knowledge_graph.graph.nodes[c].get('name')}
34 for c in starter_concepts
35 ],
36 "path": []
37 }
38
39 # Find path from user's most relevant mastered concept to goal
40 most_relevant_mastered = None
41 shortest_path = None
42
43 for mc in mastered_concepts:
44 try:
45 path = nx.shortest_path(knowledge_graph.graph, mc.id, goal_concept_id)
46 if shortest_path is None or len(path) < len(shortest_path):
47 shortest_path = path
48 most_relevant_mastered = mc
49 except nx.NetworkXNoPath:
50 continue
51
52 # If no path found, return next best concepts to learn
53 if not shortest_path:
54 # Find concepts that user hasn't mastered that have all prerequisites met
55 candidates = []
56 for node in knowledge_graph.graph.nodes:
57 if node in [c.id for c in mastered_concepts]:
58 continue # Skip already mastered concepts
59
60 prereqs = knowledge_graph.get_prerequisites(node)
61 if not prereqs or all(p in [c.id for c in mastered_concepts] for p in prereqs):
62 candidates.append(node)
63
64 return {
65 "message": "No direct path to goal. Consider these intermediate concepts:",
66 "recommended_concepts": [
67 {"id": c, "name": knowledge_graph.graph.nodes[c].get('name')}
68 for c in candidates
69 ]
70 }
71
72 # Return the learning path
73 path_concepts = []
74 for concept_id in shortest_path:
75 if concept_id not in [c.id for c in mastered_concepts]:
76 node = knowledge_graph.graph.nodes[concept_id]
77 path_concepts.append({
78 "id": concept_id,
79 "name": node.get('name'),
80 "difficulty": node.get('difficulty', 5)
81 })
82
83 return {
84 "starting_from": most_relevant_mastered.name,
85 "goal": goal_concept.name,
86 "learning_path": path_concepts
87 }

6. Deployment & Hosting

Local Deployment with Docker

Create a docker-compose.yml file in the project root:

yaml
1version: '3'
2
3services:
4 frontend:
5 build:
6 context: ./frontend
7 ports:
8 - "3000:3000"
9 depends_on:
10 - backend
11 environment:
12 - NEXT_PUBLIC_API_URL=http://backend:8000
13
14 backend:
15 build:
16 context: ./backend
17 ports:
18 - "8000:8000"
19 depends_on:
20 - postgres
21 - ollama
22 environment:
23 - DATABASE_URL=postgresql://ailearning:password@postgres/ailearning
24 - OLLAMA_BASE_URL=http://ollama:11434
25 volumes:
26 - ./backend:/app
27 - chroma_data:/app/chroma_data
28
29 postgres:
30 image: postgres:15
31 environment:
32 - POSTGRES_USER=ailearning
33 - POSTGRES_PASSWORD=password
34 - POSTGRES_DB=ailearning
35 volumes:
36 - postgres_data:/var/lib/postgresql/data
37 ports:
38 - "5432:5432"
39
40 ollama:
41 image: ollama/ollama:latest
42 volumes:
43 - ollama_models:/root/.ollama
44 ports:
45 - "11434:11434"
46 deploy:
47 resources:
48 reservations:
49 devices:
50 - driver: nvidia
51 count: 1
52 capabilities: [gpu]
53
54volumes:
55 postgres_data:
56 ollama_models:
57 chroma_data:

Create a Dockerfile for the backend:

dockerfile
1# backend/Dockerfile
2FROM python:3.10-slim
3
4WORKDIR /app
5
6COPY requirements.txt .
7RUN pip install --no-cache-dir -r requirements.txt
8
9COPY . .
10
11CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Create a Dockerfile for the frontend:

dockerfile
1# frontend/Dockerfile
2FROM node:18-alpine
3
4WORKDIR /app
5
6COPY package*.json ./
7RUN npm install
8
9COPY . .
10
11RUN npm run build
12
13CMD ["npm", "start"]

VPS Deployment

For deployment to a VPS:

  1. Set up a server with Ubuntu
  2. Install Docker and Docker Compose
  3. Clone your repository
  4. Start the services:
bash
1docker-compose up -d
  1. Set up Nginx as a reverse proxy:
nginx
1server {
2 listen 80;
3 server_name yourdomain.com;
4
5 location / {
6 proxy_pass http://localhost:3000;
7 proxy_http_version 1.1;
8 proxy_set_header Upgrade $http_upgrade;
9 proxy_set_header Connection 'upgrade';
10 proxy_set_header Host $host;
11 proxy_cache_bypass $http_upgrade;
12 }
13
14 location /api {
15 rewrite ^/api/(.*) /$1 break;
16 proxy_pass http://localhost:8000;
17 proxy_http_version 1.1;
18 proxy_set_header Upgrade $http_upgrade;
19 proxy_set_header Connection 'upgrade';
20 proxy_set_header Host $host;
21 proxy_cache_bypass $http_upgrade;
22 }
23}
  1. Set up SSL with Certbot:
bash
1sudo apt install certbot python3-certbot-nginx
2sudo certbot --nginx -d yourdomain.com

7. Next Steps

After implementing the core system, consider these next steps:

  1. Define a Comprehensive API Schema

    • Document all endpoints with OpenAPI
    • Implement strong validation rules
    • Create API clients for frontend use
  2. Enhance the Feedback Loop

    • Add metrics for lesson effectiveness
    • Implement user ratings for lessons
    • Collect and analyze exercise performance
  3. Fine-Tune RAG Prompts

    • Experiment with different context retrieval methods
    • Optimize prompt templates for different learning styles
    • Implement prompt versioning to compare effectiveness
  4. Add Comprehensive Logging

    • Track user interactions for debugging
    • Monitor system performance
    • Set up alerts for critical errors
  5. Implement User Authentication

    • Add secure login and registration
    • Support multiple user roles (learner, instructor)
    • Add profile management

This guide provides a comprehensive framework for building your personalized AI learning system with local LLMs. By following these steps, you'll create a system that can generate unique, adaptive lessons, build a knowledge graph of concepts, and provide an engaging learning experience.

The modular nature of this implementation allows you to start with core functionality and progressively enhance the system with additional features as you go. Happy building!

Sovereign AI book cover

Sovereign AI: Building Local-First Intelligent Systems

by Daniel Kliewer · Paperback · 72 pages

The hands-on guide to building AI that runs on your hardware, keeps your data private, and eliminates cloud dependence. Working code included.