Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 67 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<a href="https://daily-english-mission-ten.vercel.app/presentation"><b>🎬 Presentation</b></a> •
<a href="#-live-demo"><b>🚀 Live Demo</b></a> •
<a href="#-key-features">Features</a> •
<a href="#-ai-agent-integration-hermes--mcp">AI Agent</a> •
<a href="#-architecture">Architecture</a> •
<a href="#-quick-start">Quick Start</a> •
<a href="#-operational-modules">Modules</a> •
Expand Down Expand Up @@ -49,7 +50,7 @@

Traditional e-learning platforms rely on **passive video watching**, **superficial badges**, and **arbitrary completion percentages** that fail to build real engineering intuition.

**Engineer Learning OS** is a local-first platform designed to turn engineering course materials and technical challenges into **verifiable mastery**. It combines **zero-hallucination document extraction**, **grounded academic quizzes**, **spaced error retrieval**, and **integrated technical English** into a distraction-free modular monolith.
**Engineer Learning OS** is a local-first platform designed to turn engineering course materials and technical challenges into **verifiable mastery**. It combines **zero-hallucination document extraction**, **grounded academic quizzes**, **spaced error retrieval**, and **integrated technical English** into a distraction-free modular monolith — and it can follow you to your phone through a **local AI tutor** (Hermes Agent over MCP) on Telegram and WhatsApp.

> ⚡ **Core Philosophy:** No fake progress. Every competency level (`NOT_SEEN` ➔ `PRACTICED` ➔ `VERIFIED`) is derived purely from **auditable, reproducible evidence records**.

Expand Down Expand Up @@ -99,6 +100,40 @@ Traditional e-learning platforms rely on **passive video watching**, **superfici
- **Context-Aware Practice:** Confidence calibration, bounded assistance, semantic self-explanation, transfer evidence, and curated concept relations inform the next action.
- **Canonical Shadow Projection:** The policy reads privacy-safe IndexedDB event metadata without rewriting EventIds or independently awarding mastery.

### 📚 7. Learning Studio (`learning-studio`) — `/studio`
- **Your own courses, on your device:** import PDF, Markdown or text files; text is extracted with PDF.js in the browser, and scanned PDFs are read by **on-device OCR** (tesseract.js, French model vendored — no upload).
- **Cited answers only:** questions are answered with sentences quoted from your sources (page or section), or the Studio says the sources do not contain the answer. A Socratic mode gives graduated hints instead.
- **Study material from the sources:** flashcards, quizzes, revision sheets, mind maps, prerequisite paths, an audio overview read by a local voice, annotations, and timed **mock exams** — all marked `DERIVED_MATERIAL`, never mastery.
- **Scheduling:** Leitner ladder by default, with an optional **FSRS-4.5** scheduler (evaluated by replay on real public review logs, where it beat Leitner on log-loss).
- **Programme import with quality audit:** a pedagogical base export (courses, exercises, flashcards) becomes a course map; an automatic audit blocks guessable multiple-choice banks (correct answer always the longest, generic distractors) and copy-pasted flashcards.
- **Offline:** a service worker keeps the app usable without a network.

### 🗣️ 8. Daily English Mission (`daily-english`) — `/daily-english`
- **150 words in 5 packs** (core, networking, cybersecurity, meetings, career) with Leitner spaced review and a daily mission.
- **Hand-written professional roleplays** (stand-up, incident report, apprenticeship interview) evaluated locally, plus on-device Whisper transcription and a full local backup / restore.

---

## 🤖 AI Agent Integration (Hermes + MCP)

Engineer Learning OS ships a local **[Model Context Protocol](https://modelcontextprotocol.io/) server** (`mcp/elos-server.ts`) so a personal agent such as **Hermes Agent** can tutor you — including from **Telegram and WhatsApp** on your phone.

| Tool | What the agent can do |
|---|---|
| `elos_ask_sources` | Answer only with quoted sentences from your course files (page citations), or a Socratic question with hints |
| `elos_study_pack` | Flashcards, quiz, revision sheet, audio overview script, prerequisite paths |
| `elos_course_map` | Programme map: objectives, prerequisites, worked example, analogy, frequent mistakes, quality audit |
| `elos_course_exercise` | Multiple-choice questions built from **your own course files** for a chapter, with citations; answer revealed only after your attempt; missed questions come back first (`retry`) |
| `elos_today` · `elos_progress` · `elos_exam_plan` | What to study today, progress, exam countdown with spaced sessions (read-only snapshot) |
| `elos_correct_english` · `elos_roleplay` · `elos_vocabulary` | Local English correction, roleplays, vocabulary |
| `elos_compare_schedulers` | Replay review logs: FSRS-4.5 vs Leitner |

- **Daily rhythm:** a morning coach at 8:00 and an evening review at 19:00 (missed questions to redo) are delivered to Telegram/WhatsApp by Hermes cron jobs — deterministic messages, no model involved.
- **Two-way, read-only by design:** the app shares a minimal snapshot (`elos-state.json`); the agent writes back practice counts per chapter (`elos-practice.json`), shown in the app as assisted practice that **never changes a mastery level**.
- **Guard rails:** course files are read only from `ELOS_SOURCES_DIR`; `knowledge-private/`, secrets and path traversal are refused; answer keys are kept outside the folder the agent browses (`npm run mcp:split-base`); scanned PDFs are OCR'd on the Mac with Apple Vision and cached outside the shared folder.

Setup, privacy notes and cron recipes: **[`docs/integrations/HERMES.md`](docs/integrations/HERMES.md)**.

---

## 🖼️ Product Tour
Expand Down Expand Up @@ -135,6 +170,13 @@ flowchart TD
Review["/review (Spaced Retrieval)"]
Evidence["/evidence (Audit Trail)"]
Progress["/progress (Competency Matrix)"]
Studio["/studio (Learning Studio)"]
English["/daily-english (Missions & Roleplays)"]
end

subgraph Agent ["🤖 Local Agent Bridge"]
MCP["mcp/elos-server.ts\n(MCP stdio, 11 tools)"]
Hermes["Hermes Agent\n(Telegram · WhatsApp · cron)"]
end

subgraph Core ["🧠 Pure Business Modules (src/modules/)"]
Expand All @@ -148,6 +190,8 @@ flowchart TD
DM["deep-mastery\n(Mental Model Invariants)"]
MR["mission-runtime\n(Active Time & Session Engine)"]
SP["scientific-pedagogy\n(Evidence-Informed Policy)"]
LS["learning-studio\n(Cited Answers, Exams, Programme Audit)"]
SN["learner-snapshot\n(Agent Bridge & Coach)"]
end

subgraph Infra ["🔌 Infrastructure & Local Storage"]
Expand All @@ -161,6 +205,8 @@ flowchart TD

UI --> Core
Core --> Infra
Hermes --> MCP
MCP --> Core
```

---
Expand Down Expand Up @@ -192,7 +238,14 @@ npm test # node:test suites, no extra flag needed
```
`npm test` runs the unit and integration suites with Node's built-in test runner. A small resolver (`tests/register-loader.mjs`) is registered automatically, so the command works as written from a fresh clone.

### 4. Build for Production
### 4. Optional: connect a local agent
```bash
npm run mcp:elos # MCP server over stdio (see docs/integrations/HERMES.md)
npm run mcp:ocr # pre-OCR scanned PDFs on macOS (Apple Vision), cached locally
npm run mcp:split-base -- <base> <private dir> # move a pedagogical base's answer key out of the shared folder
```

### 5. Build for Production
```bash
npm run build
```
Expand All @@ -217,6 +270,10 @@ src/modules/
├── deep-mastery/ # Deliberate practice on fundamental invariants (CSV, encodings)
├── scientific-pedagogy/ # Evidence-traceable scheduling, assistance, calibration, transfer policy
├── technical-english/ # Spoken/written technical English & audio evidence persistence
├── learning-studio/ # Source library, cited answers, OCR import, mock exams, programme audit, chapter practice
├── learner-snapshot/ # Agent snapshot, practice summary from the agent, morning & evening coach messages
├── daily-english/ # 150-word vocabulary, Leitner review, roleplays, local correction, backup
├── local-speech/ # On-device Whisper transcription & pronunciation feedback
└── mission-runtime/ # Multi-step interactive mission runner & active time tracking
```

Expand All @@ -234,6 +291,10 @@ src/modules/
| `/evidence` | **Preuves** | Auditable chronological evidence stream of learner attempts |
| `/progress` | **Progression** | Honest competency graph derived strictly from verified evidence |
| `/daily-english` | **Daily English** | Daily interactive speaking and vocabulary practice |
| `/daily-english/roleplay` | **Roleplays** | Professional English roleplays evaluated locally |
| `/studio` | **Learning Studio** | Your own course files: cited answers, flashcards, quizzes, mock exams, audio overview |
| `/data` | **Données** | Backup / restore and the local agent (Hermes) bridge with practice done on your phone |
| `/presentation` | **Présentation** | 100-second product video with captions |

---

Expand All @@ -242,6 +303,7 @@ src/modules/
- 🔒 **Bounded data flow:** Proprietary documents, internal spreadsheets, private audio, and credentials stay outside Git and remote sync; only metadata classified `SYNC_ALLOWED` can enter the guarded outbox.
- 🧱 **Local automation is opt-in:** the NotebookLM browser-automation bridge (`POST /api/notebooklm/execute`) answers `404` and starts nothing unless `ELOS_NOTEBOOKLM_AUTOMATION=ENABLED` is set in the **server** environment. Even then it only accepts same-origin JSON requests addressed to `localhost`, and it refuses hosted platforms and proxied requests. Never enable it on a deployed instance.
- 🧪 **Synthetic Fixtures Only:** All automated tests use 100% synthetic, non-sensitive fixtures (`TRAINING_SYNTHETIC`).
- 🤖 **Agent boundaries:** the MCP server only reads the course folder you configure and refuses private folders and secrets. Anything it returns is sent by the agent to *its* model provider — the snapshot it shares never contains your sentences, transcriptions, notes or answers.
- 🌐 **Offline by Default:** Document extraction, audio recording, quiz grading, and competency derivation run entirely on your local machine without mandatory network calls.

---
Expand All @@ -250,9 +312,9 @@ src/modules/

```text
learning-os, local-first, privacy-first, edtech, deliberate-practice,
spaced-repetition, leitner-system, competency-tracking, technical-english,
english-learning, vocabulary-builder, document-extraction, docling, pdfjs,
nextjs, react, typescript, tailwindcss, indexeddb, cybersecurity-education
spaced-repetition, fsrs, competency-tracking, technical-english,
english-learning, mcp, model-context-protocol, ai-tutor, ocr, pdfjs,
nextjs, react, typescript, indexeddb, cybersecurity-education
```

---
Expand Down
18 changes: 18 additions & 0 deletions docs/integrations/HERMES.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,21 @@ hermes cron create "0 8 * * *" --name "ELOS coach du matin" --script elos-mornin

Avec une messagerie configurée dans Hermes (`hermes gateway setup`), remplace `--deliver local` par `telegram`,
`signal`… Le contenu (vocabulaire, jeux de rôle, intitulés de notions) est générique ; aucun passage de cours n’y figure.

### Révision du soir

`scripts/evening-review.ts` lit `elos-practice.json` (écrit par le serveur après chaque QCM fait avec l’agent) et
liste les chapitres pratiqués dans la journée et les questions ratées à refaire. L’apprenant répond « repose mes
questions ratées » : le tuteur appelle `elos_course_exercise` avec `retry: true`, qui repose la dernière question
ratée encore disponible. Le message ne contient que des intitulés de chapitres et des comptes.

```bash
cat > ~/.hermes/scripts/elos-evening-review.sh <<'SH'
#!/bin/bash
cd "$HOME/Desktop/Developer/daily-english-mission" || exit 1
ELOS_STATE_FILE="$HOME/ELOS-Hermes/elos-state.json" exec "$(command -v node)" --experimental-strip-types --no-warnings \
--import ./tests/register-loader.mjs scripts/evening-review.ts
SH
chmod +x ~/.hermes/scripts/elos-evening-review.sh
hermes cron create "0 19 * * *" --name "ELOS révision du soir" --script elos-evening-review.sh --no-agent --deliver telegram,whatsapp
```
22 changes: 19 additions & 3 deletions mcp/elos-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,10 +451,26 @@ export function createServer(env: NodeJS.ProcessEnv = process.env): McpServer {

server.registerTool("elos_course_exercise", {
title: "Practice exercise with answer revealed after the attempt",
description: `Practice on a programme chapter. With chapter_id: one exercise, statement only. By default (source "courses") a multiple-choice question built from the learner's own course files, with a page citation, preferring questions missed before; otherwise (or with source "base") an exercise of the pedagogical base. Multiple-choice questions that failed the quality audit are never served. With exercise_id and the learner's answer (a letter A-D or the option text for a multiple-choice question): the correction; course questions are graded and logged as practice (DERIVED_PRACTICE, never mastery). Never reveal a solution before the learner has answered. ${baseNote}`,
inputSchema: { chapter_id: z.string().optional(), exercise_id: z.string().optional(), answer: z.string().min(1).max(5000).optional(), source: z.enum(["courses", "base"]).default("courses") },
}, async ({ chapter_id, exercise_id, answer, source }) => {
description: `Practice on a programme chapter. With chapter_id: one exercise, statement only. By default (source "courses") a multiple-choice question built from the learner's own course files, with a page citation, preferring questions missed before; otherwise (or with source "base") an exercise of the pedagogical base. Multiple-choice questions that failed the quality audit are never served. With retry: true: a missed course question to redo (optionally limited to chapter_id). With exercise_id and the learner's answer (a letter A-D or the option text for a multiple-choice question): the correction; course questions are graded and logged as practice (DERIVED_PRACTICE, never mastery). Never reveal a solution before the learner has answered. ${baseNote}`,
inputSchema: { chapter_id: z.string().optional(), exercise_id: z.string().optional(), answer: z.string().min(1).max(5000).optional(), source: z.enum(["courses", "base"]).default("courses"),
retry: z.boolean().default(false).describe("true: ask again a course question whose latest answer was wrong, any chapter (most recent first)") },
}, async ({ chapter_id, exercise_id, answer, source, retry }) => {
const bases = loadBases(env);
if (retry && !exercise_id) {
const latest = new Map<string, PracticeEntry>();
for (const entry of root ? readPractice(practiceLogFile(root, env)) : []) latest.set(entry.questionId, entry);
const missed = [...latest.values()].filter((entry) => !entry.correct && (!chapter_id || entry.chapterId === chapter_id)).sort((a, b) => b.at.localeCompare(a.at));
const documents = missed.length ? await loadSources(root, env) : [];
for (const entry of missed) {
const parsed = parseSourceQuestionId(entry.questionId);
const chapter = parsed && findChapter(parsed[0]);
const question = chapter && chapterSourceQuiz(documents, chapter, 50).find((item) => item.cardId === parsed[1]);
if (!chapter || !question) continue;
return json({ exercise_id: entry.questionId, type: "qcm_from_courses", retry: true, chapter: chapter.title, missedRemaining: missed.length, prompt: question.prompt,
options: question.choices.map((choice, index) => `${String.fromCharCode(65 + index)}. ${choice}`) });
}
return json({ retry: true, message: "No missed course question waiting: suggest a new chapter (elos_course_map)." });
}
if (exercise_id?.startsWith("src:")) {
const parsed = parseSourceQuestionId(exercise_id);
const chapter = parsed && findChapter(parsed[0]);
Expand Down
12 changes: 12 additions & 0 deletions scripts/evening-review.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Prints the ELOS evening review from the practice summary the agent writes next to the
// learner snapshot (elos-practice.json). No model, no network. Scheduled by Hermes:
// see docs/integrations/HERMES.md.
import { readFileSync } from "node:fs";
import path from "node:path";
import { AGENT_PRACTICE_FILE, parseAgentPractice, type AgentPractice } from "../src/modules/learner-snapshot/agent-practice.ts";
import { buildEveningMessage } from "../src/modules/learner-snapshot/coach.ts";

const stateFile = process.env.ELOS_STATE_FILE ?? path.join(process.env.HOME ?? "", "ELOS-Hermes", "elos-state.json");
let practice: AgentPractice | null = null;
try { practice = parseAgentPractice(readFileSync(path.join(path.dirname(stateFile), AGENT_PRACTICE_FILE), "utf8")); } catch { practice = null; }
console.log(buildEveningMessage(practice, new Date()));
29 changes: 29 additions & 0 deletions src/modules/learner-snapshot/coach.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { VOCABULARY } from "../daily-english/vocabulary";
import { ROLEPLAY_SCENARIOS } from "../daily-english/roleplay";
import type { AgentPractice } from "./agent-practice";
import type { LearnerSnapshot } from "./core";

/**
Expand Down Expand Up @@ -52,3 +53,31 @@ export function buildMorningMessage(snapshot: LearnerSnapshot | null, now: Date)
lines.push("", "https://daily-english-mission-ten.vercel.app/studio");
return lines.join("\n");
}

/**
* Evening review built from the practice done with the agent (elos-practice.json):
* what to redo before sleeping, retrieval first. Counts and chapter titles only.
*/
export function buildEveningMessage(practice: AgentPractice | null, now: Date): string {
const date = now.toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long" });
const lines = [`🌙 ELOS — révision du soir, ${date}`];
const today = now.toISOString().slice(0, 10);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare attempts using the learner's local date

toISOString() derives today in UTC, while the heading and the 19:00 cron schedule use the machine's local timezone. In a negative-offset timezone—for example, New York at 19:00 in winter—UTC has already advanced to the next day, so chapters practised earlier that local day do not appear under “Aujourd’hui.” Derive the comparison date in the same local timezone used for the displayed date.

Useful? React with 👍 / 👎.

const chapters = practice?.chapters ?? [];
const practisedToday = chapters.filter((chapter) => chapter.lastAt.startsWith(today));
const retry = chapters.filter((chapter) => chapter.toRetry > 0).sort((a, b) => b.toRetry - a.toRetry || b.lastAt.localeCompare(a.lastAt));

if (practisedToday.length) {
lines.push(`✅ Aujourd’hui : ${practisedToday.map((chapter) => chapter.title).join(", ")}.`);
}
if (retry.length) {
lines.push("", "↩️ À refaire avant de dormir (un rappel le soir aide à retenir) :");
for (const chapter of retry.slice(0, 3)) lines.push(` • ${chapter.title} : ${chapter.toRetry} question${chapter.toRetry > 1 ? "s" : ""} ratée${chapter.toRetry > 1 ? "s" : ""}`);
lines.push("", "Réponds « repose mes questions ratées » pour les refaire maintenant.");
} else if (chapters.length) {
lines.push("", "👏 Aucune question ratée en attente. Réponds « un QCM sur … » avec un chapitre pour avancer.");
} else {
lines.push("", "Pas encore d’entraînement avec moi. Réponds « quel est mon programme ? » puis « un QCM sur … » pour commencer.");
}
lines.push("", "C’est de l’entraînement : la maîtrise se prouve dans ELOS, sans aide.");
return lines.join("\n");
}
Loading
Loading