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.
Daniel Kliewer
Author, Sovereign AI


Building a Personalized AI Learning System with Local LLMs
Table of Contents
- 1. Introduction
- 2. System Architecture
- 3. Tech Stack & Tools
- 4. Step-by-Step Implementation
- 5. Optimization & Expansion
- 6. Deployment & Hosting
- 7. Next Steps
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
- User requests a lesson from the frontend
- Backend queries knowledge graph and past progress
- AI layer generates a personalized, non-repetitive lesson
- Frontend displays the lesson with interactive elements
- 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:
bash1# Create project directory2mkdir ai-learning-system3cd ai-learning-system45# Create subdirectories6mkdir -p frontend backend
Backend Setup:
bash1cd backend23# Create virtual environment4python -m venv venv5source venv/bin/activate # On Windows: venv\Scripts\activate67# Install dependencies8pip install fastapi uvicorn pydantic sqlalchemy psycopg2-binary chromadb sentence-transformers networkx python-multipart910# Create basic directory structure11mkdir -p app/api app/db app/models app/services
Frontend Setup:
bash1cd ../frontend23# Initialize Next.js project4npx create-next-app@latest . --typescript --tailwind --eslint --app56# Install additional dependencies7npm 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:
python1# backend/app/models/database.py2from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, Float3from sqlalchemy.ext.declarative import declarative_base4from sqlalchemy.orm import relationship5import datetime67Base = declarative_base()89class User(Base):10 __tablename__ = "users"1112 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)1718 progress = relationship("UserProgress", back_populates="user")1920class Concept(Base):21 __tablename__ = "concepts"2223 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 scale2728 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 )3839class ConceptRelationship(Base):40 __tablename__ = "concept_relationships"4142 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 strength4748 source = relationship("Concept", foreign_keys=[source_id], back_populates="followups")49 target = relationship("Concept", foreign_keys=[target_id], back_populates="prerequisites")5051class UserProgress(Base):52 __tablename__ = "user_progress"5354 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 scale58 last_studied = Column(DateTime, default=datetime.datetime.utcnow)5960 user = relationship("User", back_populates="progress")61 concept = relationship("Concept")6263class Lesson(Base):64 __tablename__ = "lessons"6566 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)7172 exercises = relationship("Exercise", back_populates="lesson")7374class Exercise(Base):75 __tablename__ = "exercises"7677 id = Column(Integer, primary_key=True, index=True)78 lesson_id = Column(Integer, ForeignKey("lessons.id"))79 question = Column(Text)80 answer = Column(Text)8182 lesson = relationship("Lesson", back_populates="exercises")
Now, let's set up the database connection:
python1# backend/app/db/database.py2from sqlalchemy import create_engine3from sqlalchemy.ext.declarative import declarative_base4from sqlalchemy.orm import sessionmaker5import os6from dotenv import load_dotenv78load_dotenv()910DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://username:password@localhost/ai_learning")1112engine = create_engine(DATABASE_URL)13SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)1415def get_db():16 db = SessionLocal()17 try:18 yield db19 finally:20 db.close()
ChromaDB Setup
python1# backend/app/db/vector_store.py2import chromadb3from chromadb.config import Settings4import os56class 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_directory11 ))1213 # Create collections if they don't exist14 self.lesson_collection = self.client.get_or_create_collection("lessons")15 self.concept_collection = self.client.get_or_create_collection("concepts")1617 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 )2526 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 )3435 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_results40 )41 return results4243 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_results48 )49 return results5051# Singleton instance to be used throughout the app52vector_store = VectorStore()
Step 3: Knowledge Graph Implementation
Let's implement the knowledge graph using NetworkX:
python1# backend/app/services/knowledge_graph.py2import networkx as nx3from app.db.database import get_db4from app.models.database import Concept, ConceptRelationship, UserProgress5import json67class KnowledgeGraph:8 def __init__(self):9 self.graph = nx.DiGraph()10 self.load_from_database()1112 def load_from_database(self):13 """Load concept relationships from database into NetworkX graph"""14 db = next(get_db())1516 # Get all concepts17 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.difficulty24 )2526 # Get all relationships27 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.strength34 )3536 def get_prerequisites(self, concept_id):37 """Get prerequisites for a given concept"""38 if not self.graph.has_node(concept_id):39 return []4041 prerequisites = []42 for pred in self.graph.predecessors(concept_id):43 if self.graph[pred][concept_id].get('type') == 'prerequisite':44 prerequisites.append(pred)4546 return prerequisites4748 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 []5253 next_concepts = []54 for succ in self.graph.successors(concept_id):55 next_concepts.append(succ)5657 return next_concepts5859 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 []6364 try:65 path = nx.shortest_path(self.graph, start_concept, target_concept)66 return path67 except nx.NetworkXNoPath:68 return []6970 def recommend_next_concept(self, user_id):71 """Recommend next concept for user based on progress"""72 db = next(get_db())7374 # Get user's current progress75 progress_records = db.query(UserProgress).filter(76 UserProgress.user_id == user_id77 ).all()7879 # Create a dict of concept_id -> mastery_level80 mastery = {p.concept_id: p.mastery_level for p in progress_records}8182 # Find concepts user has started but not mastered83 in_progress = [cid for cid, level in mastery.items() if level < 0.8]8485 if in_progress:86 # Return the concept with lowest mastery87 return min(in_progress, key=lambda x: mastery.get(x, 0))8889 # If no concepts in progress, find new concepts where prerequisites are met90 mastered = [cid for cid, level in mastery.items() if level >= 0.8]9192 candidate_concepts = []93 for concept_id in self.graph.nodes:94 if concept_id in mastery:95 continue # Skip concepts user has already started9697 prereqs = self.get_prerequisites(concept_id)98 if not prereqs or all(p in mastered for p in prereqs):99 # All prerequisites met100 candidate_concepts.append(concept_id)101102 if not candidate_concepts:103 # If no obvious next concepts, recommend any starter concept104 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]106107 # Return easiest candidate concept (by difficulty)108 return min(candidate_concepts, key=lambda x: self.graph.nodes[x].get('difficulty', 5))109110# Create singleton instance111knowledge_graph = KnowledgeGraph()112113# Ensure graph is updated when DB changes114def refresh_knowledge_graph():115 knowledge_graph.load_from_database()
Step 4: Embedding Service
Let's create a service for generating embeddings:
python1# backend/app/services/embedding_service.py2from sentence_transformers import SentenceTransformer3import numpy as np45class EmbeddingService:6 def __init__(self, model_name="all-MiniLM-L6-v2"):7 self.model = SentenceTransformer(model_name)89 def get_embedding(self, text):10 """Generate embedding for text"""11 return self.model.encode(text).tolist()1213 def get_embeddings(self, texts):14 """Generate embeddings for multiple texts"""15 return self.model.encode(texts).tolist()1617# Create singleton instance18embedding_service = EmbeddingService()
Step 5: LLM Service with Ollama
python1# backend/app/services/llm_service.py2import requests3import json4import os5from dotenv import load_dotenv67load_dotenv()89OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")10MODEL_NAME = os.getenv("LLM_MODEL", "mistral")1112class LLMService:13 def __init__(self, base_url=OLLAMA_BASE_URL, model=MODEL_NAME):14 self.base_url = base_url15 self.model = model16 self.generate_url = f"{self.base_url}/api/generate"1718 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_tokens27 }28 }2930 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)}"3839 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 content43 context = ""44 if related_content:45 context = "Related information:\n" + "\n".join(related_content)4647 # Include previous knowledge if available48 previous = ""49 if previous_knowledge:50 previous = "The user has previously learned:\n" + "\n".join(previous_knowledge)5152 prompt = f"""53 You are an expert tutor creating a lesson about "{concept_name}".5455 Basic description of the concept: {concept_description}5657 User knowledge level: {user_level}5859 {previous}6061 {context}6263 Create a comprehensive lesson that includes:64 1. A clear explanation of {concept_name}65 2. Key points to understand66 3. 2-3 concrete examples that demonstrate the concept67 4. 3 practice exercises with answer explanations68 5. A summary of what was covered6970 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 """7374 return self.generate_text(prompt)7576 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}8081 User's answer: {user_answer}8283 Correct answer: {correct_answer}8485 Provide helpful feedback on the user's answer. Include:86 1. Whether the answer is correct, partially correct, or incorrect87 2. Explanation of any mistakes or misconceptions88 3. Guidance on how to improve their understanding89 4. Positive reinforcement for what they did correctly9091 Keep your tone encouraging and constructive.92 """9394 return self.generate_text(prompt, max_tokens=800, temperature=0.5)9596# Create singleton instance97llm_service = LLMService()
Step 6: Backend API
Let's implement the main FastAPI application:
python1# backend/app/main.py2from fastapi import FastAPI, Depends, HTTPException, UploadFile, File, Form3from fastapi.middleware.cors import CORSMiddleware4from sqlalchemy.orm import Session5import os6import json7from typing import List, Optional8from pydantic import BaseModel910from app.db.database import get_db, engine11from app.models.database import Base, User, Concept, UserProgress, Lesson, Exercise12from app.db.vector_store import vector_store13from app.services.embedding_service import embedding_service14from app.services.knowledge_graph import knowledge_graph, refresh_knowledge_graph15from app.services.llm_service import llm_service1617# Create database tables18Base.metadata.create_all(bind=engine)1920app = FastAPI(title="AI Learning System API")2122# Configure CORS23app.add_middleware(24 CORSMiddleware,25 allow_origins=["http://localhost:3000"], # Frontend URL26 allow_credentials=True,27 allow_methods=["*"],28 allow_headers=["*"],29)3031# Pydantic models for API32class ConceptBase(BaseModel):33 name: str34 description: str35 difficulty: int3637class ConceptCreate(ConceptBase):38 pass3940class ConceptRead(ConceptBase):41 id: int4243 class Config:44 orm_mode = True4546class LessonRequest(BaseModel):47 concept_id: Optional[int] = None48 user_id: int4950class LessonResponse(BaseModel):51 id: int52 content: str53 concept: ConceptRead54 exercises: List[dict]5556 class Config:57 orm_mode = True5859# Routes60@app.get("/")61def read_root():62 return {"message": "AI Learning System API"}6364@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.difficulty70 )71 db.add(db_concept)72 db.commit()73 db.refresh(db_concept)7475 # Add to vector store76 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 embedding82 )8384 # Refresh knowledge graph85 refresh_knowledge_graph()8687 return db_concept8889@app.get("/concepts/", response_model=List[ConceptRead])90def get_concepts(db: Session = Depends(get_db)):91 concepts = db.query(Concept).all()92 return concepts9394@app.post("/lessons/generate/", response_model=dict)95def generate_lesson(request: LessonRequest, db: Session = Depends(get_db)):96 # Check if user exists97 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")100101 # Determine which concept to teach102 concept_id = request.concept_id103 if not concept_id:104 # Use knowledge graph to recommend next concept105 concept_id = knowledge_graph.recommend_next_concept(request.user_id)106107 concept = db.query(Concept).filter(Concept.id == concept_id).first()108 if not concept:109 raise HTTPException(status_code=404, detail="Concept not found")110111 # Get user's level based on progress112 progress = db.query(UserProgress).filter(113 UserProgress.user_id == request.user_id,114 UserProgress.concept_id == concept_id115 ).first()116117 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"123124 # 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.8128 ).all()129130 previous_knowledge = [f"{c.name}: {c.description}" for c in mastered_concepts]131132 # Find related content using RAG133 concept_embedding = embedding_service.get_embedding(134 f"{concept.name} {concept.description}"135 )136137 related_results = vector_store.search_relevant_content(concept_embedding)138 related_content = related_results.get("documents", [])139140 # Generate lesson with LLM141 lesson_content = llm_service.generate_lesson(142 concept.name,143 concept.description,144 user_level,145 previous_knowledge,146 related_content147 )148149 # Parse exercises from the lesson (simplified)150 # In a real implementation, you'd use a more robust method to extract exercises151 exercises = []152153 # Create lesson record154 db_lesson = Lesson(155 user_id=request.user_id,156 concept_id=concept_id,157 content=lesson_content158 )159 db.add(db_lesson)160 db.commit()161 db.refresh(db_lesson)162163 # Update or create user progress164 if not progress:165 progress = UserProgress(166 user_id=request.user_id,167 concept_id=concept_id,168 mastery_level=0.1 # Initial mastery169 )170 db.add(progress)171 else:172 # Increment slightly just for viewing the lesson173 progress.mastery_level = min(progress.mastery_level + 0.05, 1.0)174175 db.commit()176177 # Return lesson data178 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.difficulty186 },187 "exercises": exercises188 }189190@app.post("/upload/")191async def upload_markdown(192 file: UploadFile = File(...),193 user_id: int = Form(...)194):195 # Read file contents196 contents = await file.read()197 text = contents.decode("utf-8")198199 # Here you would implement markdown parsing to extract concepts200 # For simplicity, we'll just assume the file contains concept data201202 # Example implementation:203 import re204205 # Extract headings as concepts206 headings = re.findall(r'## (.*?)\n', text)207208 # Process each heading as a concept209 for heading in headings:210 # Extract paragraph after heading as description211 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"213214 # Create concept215 db = next(get_db())216 concept = Concept(217 name=heading,218 description=description,219 difficulty=5 # Default difficulty220 )221 db.add(concept)222 db.commit()223 db.refresh(concept)224225 # Add to vector store226 embedding = embedding_service.get_embedding(f"{heading} {description}")227 vector_store.add_concept(228 concept.id,229 concept.name,230 concept.description,231 embedding232 )233234 # Refresh knowledge graph235 refresh_knowledge_graph()236237 return {"message": f"Processed {len(headings)} concepts from {file.filename}"}238239@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_id249 ).first()250251 if not progress:252 progress = UserProgress(253 user_id=user_id,254 concept_id=concept_id,255 mastery_level=mastery_level256 )257 db.add(progress)258 else:259 progress.mastery_level = mastery_level260261 db.commit()262 return {"status": "success"}263264if __name__ == "__main__":265 import uvicorn266 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
tsx1// frontend/app/layout.tsx2import './globals.css'3import type { Metadata } from 'next'4import { Inter } from 'next/font/google'5import Sidebar from '@/components/Sidebar'67const inter = Inter({ subsets: ['latin'] })89export const metadata: Metadata = {10 title: 'AI Learning System',11 description: 'Personalized learning powered by AI',12}1314export default function RootLayout({15 children,16}: {17 children: React.ReactNode18}) {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
tsx1// frontend/components/Sidebar.tsx2import Link from 'next/link'3import { usePathname } from 'next/navigation'45const Sidebar = () => {6 const pathname = usePathname()78 const links = [9 { href: '/', label: 'Dashboard' },10 { href: '/learn', label: 'Learn' },11 { href: '/progress', label: 'Progress' },12 { href: '/upload', label: 'Upload Content' },13 ]1415 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}3637export default Sidebar
Dashboard Page
tsx1// frontend/app/page.tsx2'use client'34import { useEffect, useState } from 'react'5import Link from 'next/link'67export default function Dashboard() {8 const [recentLessons, setRecentLessons] = useState([])9 const [recommendations, setRecommendations] = useState([])10 const [loading, setLoading] = useState(true)1112 // In a real app, you'd fetch this data from your API13 useEffect(() => {14 // Mock data for demonstration15 setRecentLessons([16 { id: 1, title: 'Introduction to Neural Networks', date: '2023-05-15' },17 { id: 2, title: 'Python Basics', date: '2023-05-12' },18 ])1920 setRecommendations([21 { id: 3, title: 'Reinforcement Learning', difficulty: 'Intermediate' },22 { id: 4, title: 'Data Preprocessing', difficulty: 'Beginner' },23 ])2425 setLoading(false)26 }, [])2728 if (loading) {29 return <div className="flex justify-center items-center h-full">Loading...</div>30 }3132 return (33 <div>34 <h1 className="text-3xl font-bold mb-6">Learning Dashboard</h1>3536 <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>5455 <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>7374 <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
tsx1// frontend/app/learn/page.tsx2'use client'34import { useState } from 'react'5import { useRouter } from 'next/navigation'67export default function LearnPage() {8 const router = useRouter()9 const [loading, setLoading] = useState(false)1011 // Mock data - in a real app you'd fetch these from your API12 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 ]1819 const startLesson = async (topicId: number) => {20 setLoading(true)2122 try {23 // In a real app, you'd make an API call to generate a lesson24 // 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()3031 // For demo purposes, we'll just navigate to a mock lesson32 router.push(`/learn/${topicId}`)33 } catch (error) {34 console.error('Error starting lesson:', error)35 } finally {36 setLoading(false)37 }38 }3940 const getRecommendedLesson = async () => {41 setLoading(true)4243 try {44 // In a real app, you'd make an API call to get a recommended lesson45 // 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()5152 // For demo, we'll randomly select a topic53 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 }6162 return (63 <div>64 <h1 className="text-3xl font-bold mb-6">Start Learning</h1>6566 <div className="mb-6">67 <button68 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>7576 <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 <button84 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 Lesson89 </button>90 </div>91 ))}92 </div>93 </div>94 </div>95 )96}
Lesson Display Component
tsx1// frontend/app/learn/[id]/page.tsx2'use client'34import { useState, useEffect } from 'react'5import ReactMarkdown from 'react-markdown'67export 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>>({})1314 useEffect(() => {15 // In a real app, fetch from your API16 // For demo, we'll use mock data17 const mockLesson = {18 id: parseInt(params.id),19 title: 'Introduction to Neural Networks',20 content: `21# Introduction to Neural Networks2223Neural networks are computational models inspired by the human brain. They consist of layers of interconnected nodes or "neurons" that process information.2425## Key Concepts2627- **Neurons**: Basic units that receive inputs, apply weights, and output signals28- **Layers**: Groups of neurons that process information sequentially29- **Activation Functions**: Functions that determine the output of a neuron30- **Weights and Biases**: Parameters that are adjusted during training3132## How Neural Networks Work3334A neural network processes data through layers:35361. Input layer receives the initial data372. Hidden layers process the data383. Output layer produces the final result3940## Examples4142### Example 1: Image Recognition4344A neural network can be trained to recognize images:45- Input: Pixel values from an image46- Process: Multiple layers extract features (edges, shapes, etc.)47- Output: Classification (e.g., "cat", "dog", "car")4849### Example 2: Natural Language Processing5051Neural networks power modern language models:52- Input: Text converted to numerical representations53- Process: Layers extract meaning and context54- Output: Generated text, translations, or classifications55 `,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 }7475 setLesson(mockLesson)76 setLoading(false)77 }, [params.id])7879 const submitAnswer = async (exerciseId: number) => {80 const userAnswer = userAnswers[exerciseId] || ''8182 // In a real app, you'd send this to your API for evaluation83 // For demo, we'll just compare with the expected answer84 const exercise = lesson.exercises.find((ex: any) => ex.id === exerciseId)8586 if (exercise) {87 const correctAnswer = exercise.answer8889 // Simple check - in a real app, use the LLM to evaluate90 let feedbackText91 if (userAnswer.toLowerCase() === correctAnswer.toLowerCase()) {92 feedbackText = "Correct! Well done."93 } else {94 feedbackText = `Not quite. The correct answer is: ${correctAnswer}`95 }9697 setFeedback({98 ...feedback,99 [exerciseId]: feedbackText100 })101 }102 }103104 if (loading) {105 return <div className="flex justify-center items-center h-full">Loading lesson...</div>106 }107108 return (109 <div>110 <h1 className="text-3xl font-bold mb-6">{lesson.title}</h1>111112 <div className="mb-6">113 <nav className="flex border-b">114 <button115 className={`py-2 px-4 ${activeTab === 'lesson' ? 'border-b-2 border-blue-500 font-medium' : ''}`}116 onClick={() => setActiveTab('lesson')}117 >118 Lesson119 </button>120 <button121 className={`py-2 px-4 ${activeTab === 'exercises' ? 'border-b-2 border-blue-500 font-medium' : ''}`}122 onClick={() => setActiveTab('exercises')}123 >124 Exercises125 </button>126 </nav>127 </div>128129 <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 <textarea142 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.value149 })}150 />151 <button152 className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700"153 onClick={() => submitAnswer(exercise.id)}154 >155 Submit Answer156 </button>157158 {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>173174 <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 Lesson177 </button>178 <button className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">179 Mark as Complete180 </button>181 </div>182 </div>183 )184}
File Upload Component
tsx1// frontend/app/upload/page.tsx2'use client'34import { useCallback, useState } from 'react'5import { useDropzone } from 'react-dropzone'67export default function UploadPage() {8 const [uploading, setUploading] = useState(false)9 const [uploadStatus, setUploadStatus] = useState<null | { success: boolean; message: string }>(null)1011 const onDrop = useCallback(async (acceptedFiles: File[]) => {12 if (acceptedFiles.length === 0) return1314 setUploading(true)15 setUploadStatus(null)1617 try {18 const file = acceptedFiles[0]1920 // Create form data for file upload21 const formData = new FormData()22 formData.append('file', file)23 formData.append('user_id', '1') // In a real app, get from auth context2425 // In a real app, send to your API26 // const response = await fetch('http://localhost:8000/upload/', {27 // method: 'POST',28 // body: formData,29 // })3031 // For demo purposes, simulate success32 await new Promise(resolve => setTimeout(resolve, 1500))3334 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 }, [])4849 const { getRootProps, getInputProps, isDragActive } = useDropzone({50 onDrop,51 accept: {52 'text/markdown': ['.md'],53 'text/plain': ['.txt']54 },55 maxFiles: 156 })5758 return (59 <div>60 <h1 className="text-3xl font-bold mb-6">Upload Learning Material</h1>6162 <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 content65 and extract concepts, examples, and exercises.66 </p>6768 <div69 {...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>8687 {uploadStatus && (88 <div89 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 )}9697 <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:
- Start the backend service:
bash1cd backend2python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
- In a separate terminal, start the frontend:
bash1cd frontend2npm run dev
- Ensure Ollama is running with your chosen model:
bash1ollama 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:
python1# backend/app/services/llm_service.py2# Add this method to the LLMService class34def 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}89 Expert explanation: {concept_description}1011 User explanation: {user_explanation}1213 As an AI tutor, evaluate how well the user has understood and explained the concept.1415 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]2122 Keep your tone encouraging and constructive.23 """2425 return self.generate_text(prompt, max_tokens=800, temperature=0.3)
Adaptive Scaling
To implement adaptive difficulty scaling:
python1# backend/app/services/knowledge_graph.py2# Add this method to KnowledgeGraph class34def get_appropriate_difficulty(self, user_id, max_difficulty=10):5 """Determine appropriate difficulty level based on user mastery"""6 db = next(get_db())78 # Get average mastery level across all concepts9 progress = db.query(UserProgress).filter(10 UserProgress.user_id == user_id11 ).all()1213 if not progress:14 return 2 # Start with easy concepts for new users1516 avg_mastery = sum(p.mastery_level for p in progress) / len(progress)1718 # Scale difficulty based on mastery (higher mastery = higher difficulty)19 # This is a simple linear scaling approach20 recommended_difficulty = int(avg_mastery * 10) + 12122 # Cap at max_difficulty23 return min(recommended_difficulty, max_difficulty)
Custom Learning Paths
Allow users to define their own learning goals:
python1# backend/app/main.py2# Add this endpoint34@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 exist11 user = db.query(User).filter(User.id == user_id).first()12 goal_concept = db.query(Concept).filter(Concept.id == goal_concept_id).first()1314 if not user or not goal_concept:15 raise HTTPException(status_code=404, detail="User or concept not found")1617 # Get user's current knowledge state18 mastered_concepts = db.query(Concept).join(UserProgress).filter(19 UserProgress.user_id == user_id,20 UserProgress.mastery_level >= 0.821 ).all()2223 # If user has no mastered concepts, find starter concepts24 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 prerequisites29 starter_concepts.append(node)3031 return {32 "starter_concepts": [33 {"id": c, "name": knowledge_graph.graph.nodes[c].get('name')}34 for c in starter_concepts35 ],36 "path": []37 }3839 # Find path from user's most relevant mastered concept to goal40 most_relevant_mastered = None41 shortest_path = None4243 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 = path48 most_relevant_mastered = mc49 except nx.NetworkXNoPath:50 continue5152 # If no path found, return next best concepts to learn53 if not shortest_path:54 # Find concepts that user hasn't mastered that have all prerequisites met55 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 concepts5960 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)6364 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 candidates69 ]70 }7172 # Return the learning path73 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 })8283 return {84 "starting_from": most_relevant_mastered.name,85 "goal": goal_concept.name,86 "learning_path": path_concepts87 }
6. Deployment & Hosting
Local Deployment with Docker
Create a docker-compose.yml file in the project root:
yaml1version: '3'23services:4 frontend:5 build:6 context: ./frontend7 ports:8 - "3000:3000"9 depends_on:10 - backend11 environment:12 - NEXT_PUBLIC_API_URL=http://backend:80001314 backend:15 build:16 context: ./backend17 ports:18 - "8000:8000"19 depends_on:20 - postgres21 - ollama22 environment:23 - DATABASE_URL=postgresql://ailearning:password@postgres/ailearning24 - OLLAMA_BASE_URL=http://ollama:1143425 volumes:26 - ./backend:/app27 - chroma_data:/app/chroma_data2829 postgres:30 image: postgres:1531 environment:32 - POSTGRES_USER=ailearning33 - POSTGRES_PASSWORD=password34 - POSTGRES_DB=ailearning35 volumes:36 - postgres_data:/var/lib/postgresql/data37 ports:38 - "5432:5432"3940 ollama:41 image: ollama/ollama:latest42 volumes:43 - ollama_models:/root/.ollama44 ports:45 - "11434:11434"46 deploy:47 resources:48 reservations:49 devices:50 - driver: nvidia51 count: 152 capabilities: [gpu]5354volumes:55 postgres_data:56 ollama_models:57 chroma_data:
Create a Dockerfile for the backend:
dockerfile1# backend/Dockerfile2FROM python:3.10-slim34WORKDIR /app56COPY requirements.txt .7RUN pip install --no-cache-dir -r requirements.txt89COPY . .1011CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Create a Dockerfile for the frontend:
dockerfile1# frontend/Dockerfile2FROM node:18-alpine34WORKDIR /app56COPY package*.json ./7RUN npm install89COPY . .1011RUN npm run build1213CMD ["npm", "start"]
VPS Deployment
For deployment to a VPS:
- Set up a server with Ubuntu
- Install Docker and Docker Compose
- Clone your repository
- Start the services:
bash1docker-compose up -d
- Set up Nginx as a reverse proxy:
nginx1server {2 listen 80;3 server_name yourdomain.com;45 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 }1314 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}
- Set up SSL with Certbot:
bash1sudo apt install certbot python3-certbot-nginx2sudo certbot --nginx -d yourdomain.com
7. Next Steps
After implementing the core system, consider these next steps:
-
Define a Comprehensive API Schema
- Document all endpoints with OpenAPI
- Implement strong validation rules
- Create API clients for frontend use
-
Enhance the Feedback Loop
- Add metrics for lesson effectiveness
- Implement user ratings for lessons
- Collect and analyze exercise performance
-
Fine-Tune RAG Prompts
- Experiment with different context retrieval methods
- Optimize prompt templates for different learning styles
- Implement prompt versioning to compare effectiveness
-
Add Comprehensive Logging
- Track user interactions for debugging
- Monitor system performance
- Set up alerts for critical errors
-
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: 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.