@machinespirits/eval 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/MobileEvalDashboard.tsx +267 -0
- package/components/comparison/DeltaAnalysisTable.tsx +137 -0
- package/components/comparison/ProfileComparisonCard.tsx +176 -0
- package/components/comparison/RecognitionABMode.tsx +385 -0
- package/components/comparison/RecognitionMetricsPanel.tsx +135 -0
- package/components/comparison/WinnerIndicator.tsx +64 -0
- package/components/comparison/index.ts +5 -0
- package/components/mobile/BottomSheet.tsx +233 -0
- package/components/mobile/DimensionBreakdown.tsx +210 -0
- package/components/mobile/DocsView.tsx +363 -0
- package/components/mobile/LogsView.tsx +481 -0
- package/components/mobile/PsychodynamicQuadrant.tsx +261 -0
- package/components/mobile/QuickTestView.tsx +1098 -0
- package/components/mobile/RecognitionTypeChart.tsx +124 -0
- package/components/mobile/RecognitionView.tsx +809 -0
- package/components/mobile/RunDetailView.tsx +261 -0
- package/components/mobile/RunHistoryView.tsx +367 -0
- package/components/mobile/ScoreRadial.tsx +211 -0
- package/components/mobile/StreamingLogPanel.tsx +230 -0
- package/components/mobile/SynthesisStrategyChart.tsx +140 -0
- package/config/interaction-eval-scenarios.yaml +832 -0
- package/config/learner-agents.yaml +248 -0
- package/docs/research/ABLATION-DIALOGUE-ROUNDS.md +52 -0
- package/docs/research/ABLATION-MODEL-SELECTION.md +53 -0
- package/docs/research/ADVANCED-EVAL-ANALYSIS.md +60 -0
- package/docs/research/ANOVA-RESULTS-2026-01-14.md +257 -0
- package/docs/research/COMPREHENSIVE-EVALUATION-PLAN.md +586 -0
- package/docs/research/COST-ANALYSIS.md +56 -0
- package/docs/research/CRITICAL-REVIEW-RECOGNITION-TUTORING.md +340 -0
- package/docs/research/DYNAMIC-VS-SCRIPTED-ANALYSIS.md +291 -0
- package/docs/research/EVAL-SYSTEM-ANALYSIS.md +306 -0
- package/docs/research/FACTORIAL-RESULTS-2026-01-14.md +301 -0
- package/docs/research/IMPLEMENTATION-PLAN-CRITIQUE-RESPONSE.md +1988 -0
- package/docs/research/LONGITUDINAL-DYADIC-EVALUATION.md +282 -0
- package/docs/research/MULTI-JUDGE-VALIDATION-2026-01-14.md +147 -0
- package/docs/research/PAPER-EXTENSION-DYADIC.md +204 -0
- package/docs/research/PAPER-UNIFIED.md +659 -0
- package/docs/research/PAPER-UNIFIED.pdf +0 -0
- package/docs/research/PROMPT-IMPROVEMENTS-2026-01-14.md +356 -0
- package/docs/research/SESSION-NOTES-2026-01-11-RECOGNITION-EVAL.md +419 -0
- package/docs/research/apa.csl +2133 -0
- package/docs/research/archive/PAPER-DRAFT-RECOGNITION-TUTORING.md +1637 -0
- package/docs/research/archive/paper-multiagent-tutor.tex +978 -0
- package/docs/research/paper-draft/full-paper.md +136 -0
- package/docs/research/paper-draft/images/pasted-image-2026-01-24T03-47-47-846Z-d76a7ae2.png +0 -0
- package/docs/research/paper-draft/references.bib +515 -0
- package/docs/research/transcript-baseline.md +139 -0
- package/docs/research/transcript-recognition-multiagent.md +187 -0
- package/hooks/useEvalData.ts +625 -0
- package/index.js +27 -0
- package/package.json +73 -0
- package/routes/evalRoutes.js +3002 -0
- package/scripts/advanced-eval-analysis.js +351 -0
- package/scripts/analyze-eval-costs.js +378 -0
- package/scripts/analyze-eval-results.js +513 -0
- package/scripts/analyze-interaction-evals.js +368 -0
- package/server-init.js +45 -0
- package/server.js +162 -0
- package/services/benchmarkService.js +1892 -0
- package/services/evaluationRunner.js +739 -0
- package/services/evaluationStore.js +1121 -0
- package/services/learnerConfigLoader.js +385 -0
- package/services/learnerTutorInteractionEngine.js +857 -0
- package/services/memory/learnerMemoryService.js +1227 -0
- package/services/memory/learnerWritingPad.js +577 -0
- package/services/memory/tutorWritingPad.js +674 -0
- package/services/promptRecommendationService.js +493 -0
- package/services/rubricEvaluator.js +826 -0
|
@@ -0,0 +1,1227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learner Memory Service
|
|
3
|
+
*
|
|
4
|
+
* Manages persistent learner knowledge base including:
|
|
5
|
+
* - Concept understanding states
|
|
6
|
+
* - Episodic memory (breakthroughs, struggles, insights)
|
|
7
|
+
* - Session summaries
|
|
8
|
+
* - Active threads (unresolved questions)
|
|
9
|
+
* - Personal definitions and connections
|
|
10
|
+
* - Learner preferences
|
|
11
|
+
*
|
|
12
|
+
* This service provides the foundation for cross-session continuity
|
|
13
|
+
* and multi-agent deliberation context.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import crypto from 'crypto';
|
|
17
|
+
import { getDb } from '../dbService.js';
|
|
18
|
+
|
|
19
|
+
// Get shared database connection
|
|
20
|
+
const db = getDb();
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Database Schema
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const LEARNER_MEMORY_TABLES = [
|
|
27
|
+
'learner_memory', 'concept_states', 'episodes', 'tutor_session_summaries',
|
|
28
|
+
'threads', 'personal_definitions', 'connections', 'learner_preferences',
|
|
29
|
+
'learning_milestones', 'agent_cost_log'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const createSchemaSQL = `
|
|
33
|
+
-- Learner Knowledge Base (root record)
|
|
34
|
+
CREATE TABLE IF NOT EXISTS learner_memory (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
learner_id TEXT NOT NULL UNIQUE,
|
|
37
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
38
|
+
last_session TEXT,
|
|
39
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_learner_memory_learner ON learner_memory(learner_id);
|
|
43
|
+
|
|
44
|
+
-- Concept Understanding States
|
|
45
|
+
CREATE TABLE IF NOT EXISTS concept_states (
|
|
46
|
+
id TEXT PRIMARY KEY,
|
|
47
|
+
learner_id TEXT NOT NULL,
|
|
48
|
+
concept_id TEXT NOT NULL,
|
|
49
|
+
label TEXT NOT NULL,
|
|
50
|
+
level TEXT DEFAULT 'unencountered' CHECK(level IN ('unencountered', 'exposed', 'developing', 'proficient', 'mastered')),
|
|
51
|
+
confidence REAL DEFAULT 0.5,
|
|
52
|
+
calibration REAL DEFAULT 0.5,
|
|
53
|
+
last_engaged TEXT,
|
|
54
|
+
engagement_count INTEGER DEFAULT 0,
|
|
55
|
+
decay_rate REAL DEFAULT 0.1,
|
|
56
|
+
sources TEXT DEFAULT '[]',
|
|
57
|
+
struggles TEXT DEFAULT '[]',
|
|
58
|
+
breakthroughs TEXT DEFAULT '[]',
|
|
59
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
60
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
61
|
+
UNIQUE(learner_id, concept_id),
|
|
62
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_concept_states_learner ON concept_states(learner_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_concept_states_concept ON concept_states(concept_id);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_concept_states_level ON concept_states(level);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_concept_states_last_engaged ON concept_states(last_engaged);
|
|
69
|
+
|
|
70
|
+
-- Episodic Memory
|
|
71
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
72
|
+
id TEXT PRIMARY KEY,
|
|
73
|
+
learner_id TEXT NOT NULL,
|
|
74
|
+
session_id TEXT,
|
|
75
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
76
|
+
type TEXT NOT NULL CHECK(type IN ('breakthrough', 'struggle', 'insight', 'question', 'connection', 'misconception', 'emotional', 'metacognitive')),
|
|
77
|
+
content TEXT NOT NULL,
|
|
78
|
+
context TEXT,
|
|
79
|
+
concepts TEXT DEFAULT '[]',
|
|
80
|
+
embedding BLOB,
|
|
81
|
+
importance REAL DEFAULT 0.5,
|
|
82
|
+
retrieval_count INTEGER DEFAULT 0,
|
|
83
|
+
agent_notes TEXT DEFAULT '[]',
|
|
84
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
85
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_learner ON episodes(learner_id);
|
|
89
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_session ON episodes(session_id);
|
|
90
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_type ON episodes(type);
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_timestamp ON episodes(timestamp);
|
|
92
|
+
CREATE INDEX IF NOT EXISTS idx_episodes_importance ON episodes(importance);
|
|
93
|
+
|
|
94
|
+
-- Tutor Session Summaries (for tutor memory, separate from user-facing session_summaries)
|
|
95
|
+
CREATE TABLE IF NOT EXISTS tutor_session_summaries (
|
|
96
|
+
id TEXT PRIMARY KEY,
|
|
97
|
+
learner_id TEXT NOT NULL,
|
|
98
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
99
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
100
|
+
duration INTEGER DEFAULT 0,
|
|
101
|
+
topics TEXT DEFAULT '[]',
|
|
102
|
+
concepts_touched TEXT DEFAULT '[]',
|
|
103
|
+
key_insights TEXT DEFAULT '[]',
|
|
104
|
+
unresolved_questions TEXT DEFAULT '[]',
|
|
105
|
+
scaffolding_level_avg REAL DEFAULT 2.0,
|
|
106
|
+
engagement_level TEXT DEFAULT 'engaged' CHECK(engagement_level IN ('struggling', 'engaged', 'flowing', 'disengaged')),
|
|
107
|
+
emotional_arc TEXT,
|
|
108
|
+
message_count INTEGER DEFAULT 0,
|
|
109
|
+
tokens_used INTEGER DEFAULT 0,
|
|
110
|
+
agent_calls TEXT DEFAULT '[]',
|
|
111
|
+
narrative_summary TEXT,
|
|
112
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
113
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_tutor_session_summaries_learner ON tutor_session_summaries(learner_id);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_tutor_session_summaries_timestamp ON tutor_session_summaries(timestamp);
|
|
118
|
+
|
|
119
|
+
-- Active Threads (Unresolved Questions)
|
|
120
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
learner_id TEXT NOT NULL,
|
|
123
|
+
topic TEXT NOT NULL,
|
|
124
|
+
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'resolved', 'dormant')),
|
|
125
|
+
origin_session TEXT,
|
|
126
|
+
last_touched TEXT DEFAULT (datetime('now')),
|
|
127
|
+
mentions TEXT DEFAULT '[]',
|
|
128
|
+
question TEXT NOT NULL,
|
|
129
|
+
partial_answers TEXT DEFAULT '[]',
|
|
130
|
+
related_concepts TEXT DEFAULT '[]',
|
|
131
|
+
student_interest REAL DEFAULT 0.5,
|
|
132
|
+
pedagogical_importance REAL DEFAULT 0.5,
|
|
133
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
134
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
135
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_threads_learner ON threads(learner_id);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_threads_status ON threads(status);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_threads_last_touched ON threads(last_touched);
|
|
141
|
+
|
|
142
|
+
-- Personal Definitions (Lexicon)
|
|
143
|
+
CREATE TABLE IF NOT EXISTS personal_definitions (
|
|
144
|
+
id TEXT PRIMARY KEY,
|
|
145
|
+
learner_id TEXT NOT NULL,
|
|
146
|
+
term TEXT NOT NULL,
|
|
147
|
+
definition TEXT NOT NULL,
|
|
148
|
+
confidence REAL DEFAULT 0.5,
|
|
149
|
+
evolution TEXT DEFAULT '[]',
|
|
150
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
151
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
152
|
+
UNIQUE(learner_id, term),
|
|
153
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_personal_definitions_learner ON personal_definitions(learner_id);
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_personal_definitions_term ON personal_definitions(term);
|
|
158
|
+
|
|
159
|
+
-- Concept Connections
|
|
160
|
+
CREATE TABLE IF NOT EXISTS connections (
|
|
161
|
+
id TEXT PRIMARY KEY,
|
|
162
|
+
learner_id TEXT NOT NULL,
|
|
163
|
+
concept_a TEXT NOT NULL,
|
|
164
|
+
concept_b TEXT NOT NULL,
|
|
165
|
+
relationship TEXT NOT NULL,
|
|
166
|
+
source TEXT DEFAULT 'student' CHECK(source IN ('student', 'tutor_suggested', 'discovered_together')),
|
|
167
|
+
strength REAL DEFAULT 0.5,
|
|
168
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
169
|
+
UNIQUE(learner_id, concept_a, concept_b),
|
|
170
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_connections_learner ON connections(learner_id);
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_connections_concepts ON connections(concept_a, concept_b);
|
|
175
|
+
|
|
176
|
+
-- Learner Preferences
|
|
177
|
+
CREATE TABLE IF NOT EXISTS learner_preferences (
|
|
178
|
+
learner_id TEXT PRIMARY KEY,
|
|
179
|
+
prefers_examples_first INTEGER DEFAULT 1,
|
|
180
|
+
prefers_socratic_mode INTEGER DEFAULT 0,
|
|
181
|
+
tolerance_for_challenge REAL DEFAULT 0.5,
|
|
182
|
+
best_time_of_day TEXT,
|
|
183
|
+
typical_session_length INTEGER DEFAULT 30,
|
|
184
|
+
preferred_pace TEXT DEFAULT 'moderate' CHECK(preferred_pace IN ('slow', 'moderate', 'fast')),
|
|
185
|
+
responds_to_encouragement INTEGER DEFAULT 1,
|
|
186
|
+
needs_normalization INTEGER DEFAULT 1,
|
|
187
|
+
effective_strategies TEXT DEFAULT '[]',
|
|
188
|
+
preferred_architecture TEXT DEFAULT 'unified',
|
|
189
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
190
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
-- Learning Milestones
|
|
194
|
+
CREATE TABLE IF NOT EXISTS learning_milestones (
|
|
195
|
+
id TEXT PRIMARY KEY,
|
|
196
|
+
learner_id TEXT NOT NULL,
|
|
197
|
+
type TEXT NOT NULL CHECK(type IN ('first_breakthrough', 'concept_mastered', 'connection_made', 'independence_achieved', 'teaching_moment', 'streak', 'deep_engagement', 'recovery')),
|
|
198
|
+
title TEXT NOT NULL,
|
|
199
|
+
description TEXT,
|
|
200
|
+
concept_id TEXT,
|
|
201
|
+
course_id TEXT,
|
|
202
|
+
achieved_at TEXT DEFAULT (datetime('now')),
|
|
203
|
+
acknowledged INTEGER DEFAULT 0,
|
|
204
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
CREATE INDEX IF NOT EXISTS idx_learning_milestones_learner ON learning_milestones(learner_id);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_learning_milestones_type ON learning_milestones(type);
|
|
209
|
+
CREATE INDEX IF NOT EXISTS idx_learning_milestones_acknowledged ON learning_milestones(acknowledged);
|
|
210
|
+
|
|
211
|
+
-- Agent Cost Tracking
|
|
212
|
+
CREATE TABLE IF NOT EXISTS agent_cost_log (
|
|
213
|
+
id TEXT PRIMARY KEY,
|
|
214
|
+
learner_id TEXT NOT NULL,
|
|
215
|
+
session_id TEXT,
|
|
216
|
+
agent_role TEXT NOT NULL,
|
|
217
|
+
architecture TEXT NOT NULL,
|
|
218
|
+
input_tokens INTEGER DEFAULT 0,
|
|
219
|
+
output_tokens INTEGER DEFAULT 0,
|
|
220
|
+
latency_ms INTEGER DEFAULT 0,
|
|
221
|
+
model TEXT,
|
|
222
|
+
timestamp TEXT DEFAULT (datetime('now')),
|
|
223
|
+
FOREIGN KEY (learner_id) REFERENCES users(id) ON DELETE CASCADE
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
CREATE INDEX IF NOT EXISTS idx_agent_cost_log_learner ON agent_cost_log(learner_id);
|
|
227
|
+
CREATE INDEX IF NOT EXISTS idx_agent_cost_log_session ON agent_cost_log(session_id);
|
|
228
|
+
CREATE INDEX IF NOT EXISTS idx_agent_cost_log_timestamp ON agent_cost_log(timestamp);
|
|
229
|
+
`;
|
|
230
|
+
|
|
231
|
+
// Try to create schema; if mismatch, drop and recreate
|
|
232
|
+
try {
|
|
233
|
+
db.exec(createSchemaSQL);
|
|
234
|
+
|
|
235
|
+
// Verify schema has correct columns (may exist with old column names)
|
|
236
|
+
const testQuery = db.prepare(`SELECT learner_id FROM tutor_session_summaries LIMIT 0`);
|
|
237
|
+
testQuery.run(); // Will throw if learner_id column doesn't exist
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (err.message?.includes('no such column') || err.message?.includes('SQLITE_ERROR')) {
|
|
240
|
+
console.log('[LearnerMemory] Schema mismatch detected, recreating tables...');
|
|
241
|
+
for (const table of LEARNER_MEMORY_TABLES) {
|
|
242
|
+
try {
|
|
243
|
+
db.exec(`DROP TABLE IF EXISTS ${table}`);
|
|
244
|
+
} catch { /* ignore */ }
|
|
245
|
+
}
|
|
246
|
+
db.exec(createSchemaSQL);
|
|
247
|
+
console.log('[LearnerMemory] Tables recreated successfully');
|
|
248
|
+
} else {
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// Helper Functions
|
|
255
|
+
// ============================================================================
|
|
256
|
+
|
|
257
|
+
const generateId = () => crypto.randomUUID();
|
|
258
|
+
|
|
259
|
+
const parseJSON = (str, defaultValue = []) => {
|
|
260
|
+
if (!str) return defaultValue;
|
|
261
|
+
try {
|
|
262
|
+
return JSON.parse(str);
|
|
263
|
+
} catch {
|
|
264
|
+
return defaultValue;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const stringifyJSON = (obj) => JSON.stringify(obj || []);
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Learner Memory CRUD
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get or create learner memory record
|
|
276
|
+
*/
|
|
277
|
+
export const getOrCreateLearnerMemory = (learnerId) => {
|
|
278
|
+
let memory = db.prepare('SELECT * FROM learner_memory WHERE learner_id = ?').get(learnerId);
|
|
279
|
+
|
|
280
|
+
if (!memory) {
|
|
281
|
+
const id = generateId();
|
|
282
|
+
db.prepare(`
|
|
283
|
+
INSERT INTO learner_memory (id, learner_id)
|
|
284
|
+
VALUES (?, ?)
|
|
285
|
+
`).run(id, learnerId);
|
|
286
|
+
memory = db.prepare('SELECT * FROM learner_memory WHERE id = ?').get(id);
|
|
287
|
+
|
|
288
|
+
// Also create default preferences
|
|
289
|
+
db.prepare(`
|
|
290
|
+
INSERT OR IGNORE INTO learner_preferences (learner_id)
|
|
291
|
+
VALUES (?)
|
|
292
|
+
`).run(learnerId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return memory;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Update last session timestamp
|
|
300
|
+
*/
|
|
301
|
+
export const updateLastSession = (learnerId, sessionId) => {
|
|
302
|
+
db.prepare(`
|
|
303
|
+
UPDATE learner_memory
|
|
304
|
+
SET last_session = ?
|
|
305
|
+
WHERE learner_id = ?
|
|
306
|
+
`).run(sessionId, learnerId);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// ============================================================================
|
|
310
|
+
// Concept States
|
|
311
|
+
// ============================================================================
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Get concept state for a learner
|
|
315
|
+
*/
|
|
316
|
+
export const getConceptState = (learnerId, conceptId) => {
|
|
317
|
+
const row = db.prepare(`
|
|
318
|
+
SELECT * FROM concept_states
|
|
319
|
+
WHERE learner_id = ? AND concept_id = ?
|
|
320
|
+
`).get(learnerId, conceptId);
|
|
321
|
+
|
|
322
|
+
if (!row) return null;
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
...row,
|
|
326
|
+
sources: parseJSON(row.sources),
|
|
327
|
+
struggles: parseJSON(row.struggles),
|
|
328
|
+
breakthroughs: parseJSON(row.breakthroughs),
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get all concept states for a learner
|
|
334
|
+
*/
|
|
335
|
+
export const getAllConceptStates = (learnerId) => {
|
|
336
|
+
const rows = db.prepare(`
|
|
337
|
+
SELECT * FROM concept_states
|
|
338
|
+
WHERE learner_id = ?
|
|
339
|
+
ORDER BY last_engaged DESC
|
|
340
|
+
`).all(learnerId);
|
|
341
|
+
|
|
342
|
+
return rows.map(row => ({
|
|
343
|
+
...row,
|
|
344
|
+
sources: parseJSON(row.sources),
|
|
345
|
+
struggles: parseJSON(row.struggles),
|
|
346
|
+
breakthroughs: parseJSON(row.breakthroughs),
|
|
347
|
+
}));
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get concepts due for review (spaced repetition)
|
|
352
|
+
*/
|
|
353
|
+
export const getConceptsDueForReview = (learnerId, limit = 5) => {
|
|
354
|
+
// Simple algorithm: concepts not engaged recently with decay factor
|
|
355
|
+
const rows = db.prepare(`
|
|
356
|
+
SELECT *,
|
|
357
|
+
julianday('now') - julianday(last_engaged) AS days_since,
|
|
358
|
+
(julianday('now') - julianday(last_engaged)) * decay_rate AS review_urgency
|
|
359
|
+
FROM concept_states
|
|
360
|
+
WHERE learner_id = ?
|
|
361
|
+
AND level IN ('exposed', 'developing', 'proficient')
|
|
362
|
+
AND last_engaged IS NOT NULL
|
|
363
|
+
ORDER BY review_urgency DESC
|
|
364
|
+
LIMIT ?
|
|
365
|
+
`).all(learnerId, limit);
|
|
366
|
+
|
|
367
|
+
return rows.map(row => ({
|
|
368
|
+
...row,
|
|
369
|
+
sources: parseJSON(row.sources),
|
|
370
|
+
struggles: parseJSON(row.struggles),
|
|
371
|
+
breakthroughs: parseJSON(row.breakthroughs),
|
|
372
|
+
}));
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Create or update concept state
|
|
377
|
+
*/
|
|
378
|
+
export const upsertConceptState = (learnerIdOrData, conceptIdArg, dataArg) => {
|
|
379
|
+
let learnerId = learnerIdOrData;
|
|
380
|
+
let conceptId = conceptIdArg;
|
|
381
|
+
let data = dataArg || {};
|
|
382
|
+
|
|
383
|
+
if (learnerIdOrData && typeof learnerIdOrData === 'object' && !Array.isArray(learnerIdOrData)) {
|
|
384
|
+
learnerId = learnerIdOrData.learnerId;
|
|
385
|
+
conceptId = learnerIdOrData.conceptId;
|
|
386
|
+
data = learnerIdOrData;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!learnerId || !conceptId) return null;
|
|
390
|
+
|
|
391
|
+
const existing = getConceptState(learnerId, conceptId);
|
|
392
|
+
|
|
393
|
+
if (existing) {
|
|
394
|
+
// Update existing
|
|
395
|
+
const updates = [];
|
|
396
|
+
const params = [];
|
|
397
|
+
|
|
398
|
+
if (data.label !== undefined) { updates.push('label = ?'); params.push(data.label); }
|
|
399
|
+
if (data.level !== undefined) { updates.push('level = ?'); params.push(data.level); }
|
|
400
|
+
if (data.confidence !== undefined) { updates.push('confidence = ?'); params.push(data.confidence); }
|
|
401
|
+
if (data.calibration !== undefined) { updates.push('calibration = ?'); params.push(data.calibration); }
|
|
402
|
+
if (data.decayRate !== undefined) { updates.push('decay_rate = ?'); params.push(data.decayRate); }
|
|
403
|
+
|
|
404
|
+
// Always update engagement
|
|
405
|
+
updates.push('last_engaged = datetime("now")');
|
|
406
|
+
updates.push('engagement_count = engagement_count + 1');
|
|
407
|
+
updates.push('updated_at = datetime("now")');
|
|
408
|
+
|
|
409
|
+
// Handle array updates
|
|
410
|
+
if (data.addSource) {
|
|
411
|
+
const sources = existing.sources.includes(data.addSource)
|
|
412
|
+
? existing.sources
|
|
413
|
+
: [...existing.sources, data.addSource];
|
|
414
|
+
updates.push('sources = ?');
|
|
415
|
+
params.push(stringifyJSON(sources));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (data.addStruggle) {
|
|
419
|
+
const struggle = { ...data.addStruggle, timestamp: new Date().toISOString() };
|
|
420
|
+
const struggles = [...existing.struggles, struggle];
|
|
421
|
+
updates.push('struggles = ?');
|
|
422
|
+
params.push(stringifyJSON(struggles));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (data.addBreakthrough) {
|
|
426
|
+
const breakthrough = { ...data.addBreakthrough, timestamp: new Date().toISOString() };
|
|
427
|
+
const breakthroughs = [...existing.breakthroughs, breakthrough];
|
|
428
|
+
updates.push('breakthroughs = ?');
|
|
429
|
+
params.push(stringifyJSON(breakthroughs));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
params.push(learnerId, conceptId);
|
|
433
|
+
|
|
434
|
+
db.prepare(`
|
|
435
|
+
UPDATE concept_states
|
|
436
|
+
SET ${updates.join(', ')}
|
|
437
|
+
WHERE learner_id = ? AND concept_id = ?
|
|
438
|
+
`).run(...params);
|
|
439
|
+
|
|
440
|
+
return getConceptState(learnerId, conceptId);
|
|
441
|
+
} else {
|
|
442
|
+
// Create new
|
|
443
|
+
const id = generateId();
|
|
444
|
+
db.prepare(`
|
|
445
|
+
INSERT INTO concept_states (
|
|
446
|
+
id, learner_id, concept_id, label, level, confidence,
|
|
447
|
+
last_engaged, sources, struggles, breakthroughs
|
|
448
|
+
)
|
|
449
|
+
VALUES (?, ?, ?, ?, ?, ?, datetime('now'), ?, ?, ?)
|
|
450
|
+
`).run(
|
|
451
|
+
id,
|
|
452
|
+
learnerId,
|
|
453
|
+
conceptId,
|
|
454
|
+
data.label || conceptId,
|
|
455
|
+
data.level || 'exposed',
|
|
456
|
+
data.confidence || 0.5,
|
|
457
|
+
stringifyJSON(data.addSource ? [data.addSource] : []),
|
|
458
|
+
stringifyJSON(data.addStruggle ? [{ ...data.addStruggle, timestamp: new Date().toISOString() }] : []),
|
|
459
|
+
stringifyJSON(data.addBreakthrough ? [{ ...data.addBreakthrough, timestamp: new Date().toISOString() }] : [])
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
return getConceptState(learnerId, conceptId);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// ============================================================================
|
|
467
|
+
// Episodes
|
|
468
|
+
// ============================================================================
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Create an episode
|
|
472
|
+
*/
|
|
473
|
+
export const createEpisode = (data) => {
|
|
474
|
+
const id = generateId();
|
|
475
|
+
db.prepare(`
|
|
476
|
+
INSERT INTO episodes (
|
|
477
|
+
id, learner_id, session_id, type, content, context,
|
|
478
|
+
concepts, importance, agent_notes
|
|
479
|
+
)
|
|
480
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
481
|
+
`).run(
|
|
482
|
+
id,
|
|
483
|
+
data.learnerId,
|
|
484
|
+
data.sessionId,
|
|
485
|
+
data.type,
|
|
486
|
+
data.content,
|
|
487
|
+
data.context || '',
|
|
488
|
+
stringifyJSON(data.concepts || []),
|
|
489
|
+
data.importance || 0.5,
|
|
490
|
+
stringifyJSON(data.agentNotes || [])
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
return db.prepare('SELECT * FROM episodes WHERE id = ?').get(id);
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get recent episodes for a learner
|
|
498
|
+
*/
|
|
499
|
+
export const getRecentEpisodes = (learnerId, limit = 20) => {
|
|
500
|
+
const rows = db.prepare(`
|
|
501
|
+
SELECT * FROM episodes
|
|
502
|
+
WHERE learner_id = ?
|
|
503
|
+
ORDER BY timestamp DESC
|
|
504
|
+
LIMIT ?
|
|
505
|
+
`).all(learnerId, limit);
|
|
506
|
+
|
|
507
|
+
return rows.map(row => ({
|
|
508
|
+
...row,
|
|
509
|
+
concepts: parseJSON(row.concepts),
|
|
510
|
+
agentNotes: parseJSON(row.agent_notes),
|
|
511
|
+
}));
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Get episodes by type
|
|
516
|
+
*/
|
|
517
|
+
export const getEpisodesByType = (learnerId, type, limit = 10) => {
|
|
518
|
+
const rows = db.prepare(`
|
|
519
|
+
SELECT * FROM episodes
|
|
520
|
+
WHERE learner_id = ? AND type = ?
|
|
521
|
+
ORDER BY timestamp DESC
|
|
522
|
+
LIMIT ?
|
|
523
|
+
`).all(learnerId, type, limit);
|
|
524
|
+
|
|
525
|
+
return rows.map(row => ({
|
|
526
|
+
...row,
|
|
527
|
+
concepts: parseJSON(row.concepts),
|
|
528
|
+
agentNotes: parseJSON(row.agent_notes),
|
|
529
|
+
}));
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Get important episodes (for context injection)
|
|
534
|
+
*/
|
|
535
|
+
export const getImportantEpisodes = (learnerId, minImportance = 0.7, limit = 10) => {
|
|
536
|
+
const rows = db.prepare(`
|
|
537
|
+
SELECT * FROM episodes
|
|
538
|
+
WHERE learner_id = ? AND importance >= ?
|
|
539
|
+
ORDER BY importance DESC, timestamp DESC
|
|
540
|
+
LIMIT ?
|
|
541
|
+
`).all(learnerId, minImportance, limit);
|
|
542
|
+
|
|
543
|
+
return rows.map(row => ({
|
|
544
|
+
...row,
|
|
545
|
+
concepts: parseJSON(row.concepts),
|
|
546
|
+
agentNotes: parseJSON(row.agent_notes),
|
|
547
|
+
}));
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Increment retrieval count (when episode is surfaced in context)
|
|
552
|
+
*/
|
|
553
|
+
export const incrementEpisodeRetrieval = (episodeId) => {
|
|
554
|
+
db.prepare(`
|
|
555
|
+
UPDATE episodes
|
|
556
|
+
SET retrieval_count = retrieval_count + 1
|
|
557
|
+
WHERE id = ?
|
|
558
|
+
`).run(episodeId);
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Decay episode importance over time
|
|
563
|
+
*/
|
|
564
|
+
export const decayEpisodeImportance = (olderThanDays = 30, decayFactor = 0.9) => {
|
|
565
|
+
db.prepare(`
|
|
566
|
+
UPDATE episodes
|
|
567
|
+
SET importance = importance * ?
|
|
568
|
+
WHERE timestamp < datetime('now', '-' || ? || ' days')
|
|
569
|
+
AND importance > 0.1
|
|
570
|
+
`).run(decayFactor, olderThanDays);
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// ============================================================================
|
|
574
|
+
// Session Summaries
|
|
575
|
+
// ============================================================================
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Create a session summary
|
|
579
|
+
*/
|
|
580
|
+
export const createSessionSummary = (data) => {
|
|
581
|
+
const id = generateId();
|
|
582
|
+
db.prepare(`
|
|
583
|
+
INSERT INTO tutor_session_summaries (
|
|
584
|
+
id, learner_id, session_id, duration, topics, concepts_touched,
|
|
585
|
+
key_insights, unresolved_questions, scaffolding_level_avg,
|
|
586
|
+
engagement_level, emotional_arc, message_count, tokens_used,
|
|
587
|
+
agent_calls, narrative_summary
|
|
588
|
+
)
|
|
589
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
590
|
+
`).run(
|
|
591
|
+
id,
|
|
592
|
+
data.learnerId,
|
|
593
|
+
data.sessionId,
|
|
594
|
+
data.duration || 0,
|
|
595
|
+
stringifyJSON(data.topics || []),
|
|
596
|
+
stringifyJSON(data.conceptsTouched || []),
|
|
597
|
+
stringifyJSON(data.keyInsights || []),
|
|
598
|
+
stringifyJSON(data.unresolvedQuestions || []),
|
|
599
|
+
data.scaffoldingLevelAvg || 2.0,
|
|
600
|
+
data.engagementLevel || 'engaged',
|
|
601
|
+
data.emotionalArc || '',
|
|
602
|
+
data.messageCount || 0,
|
|
603
|
+
data.tokensUsed || 0,
|
|
604
|
+
stringifyJSON(data.agentCalls || []),
|
|
605
|
+
data.narrativeSummary || ''
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
return getSessionSummary(data.sessionId);
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Get session summary
|
|
613
|
+
*/
|
|
614
|
+
export const getSessionSummary = (sessionId) => {
|
|
615
|
+
const row = db.prepare(`
|
|
616
|
+
SELECT * FROM tutor_session_summaries
|
|
617
|
+
WHERE session_id = ?
|
|
618
|
+
`).get(sessionId);
|
|
619
|
+
|
|
620
|
+
if (!row) return null;
|
|
621
|
+
|
|
622
|
+
return {
|
|
623
|
+
...row,
|
|
624
|
+
topics: parseJSON(row.topics),
|
|
625
|
+
conceptsTouched: parseJSON(row.concepts_touched),
|
|
626
|
+
keyInsights: parseJSON(row.key_insights),
|
|
627
|
+
unresolvedQuestions: parseJSON(row.unresolved_questions),
|
|
628
|
+
agentCalls: parseJSON(row.agent_calls),
|
|
629
|
+
};
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Get recent session summaries
|
|
634
|
+
*/
|
|
635
|
+
export const getRecentSessionSummaries = (learnerId, limit = 5) => {
|
|
636
|
+
const rows = db.prepare(`
|
|
637
|
+
SELECT * FROM tutor_session_summaries
|
|
638
|
+
WHERE learner_id = ?
|
|
639
|
+
ORDER BY timestamp DESC
|
|
640
|
+
LIMIT ?
|
|
641
|
+
`).all(learnerId, limit);
|
|
642
|
+
|
|
643
|
+
return rows.map(row => ({
|
|
644
|
+
...row,
|
|
645
|
+
topics: parseJSON(row.topics),
|
|
646
|
+
conceptsTouched: parseJSON(row.concepts_touched),
|
|
647
|
+
keyInsights: parseJSON(row.key_insights),
|
|
648
|
+
unresolvedQuestions: parseJSON(row.unresolved_questions),
|
|
649
|
+
agentCalls: parseJSON(row.agent_calls),
|
|
650
|
+
}));
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
// ============================================================================
|
|
654
|
+
// Threads
|
|
655
|
+
// ============================================================================
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Create a thread
|
|
659
|
+
*/
|
|
660
|
+
export const createThread = (data) => {
|
|
661
|
+
const id = generateId();
|
|
662
|
+
db.prepare(`
|
|
663
|
+
INSERT INTO threads (
|
|
664
|
+
id, learner_id, topic, question, origin_session,
|
|
665
|
+
related_concepts, student_interest, pedagogical_importance
|
|
666
|
+
)
|
|
667
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
668
|
+
`).run(
|
|
669
|
+
id,
|
|
670
|
+
data.learnerId,
|
|
671
|
+
data.topic,
|
|
672
|
+
data.question,
|
|
673
|
+
data.originSession,
|
|
674
|
+
stringifyJSON(data.relatedConcepts || []),
|
|
675
|
+
data.studentInterest || 0.5,
|
|
676
|
+
data.pedagogicalImportance || 0.5
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
return getThread(id);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Get thread by ID
|
|
684
|
+
*/
|
|
685
|
+
export const getThread = (threadId) => {
|
|
686
|
+
const row = db.prepare('SELECT * FROM threads WHERE id = ?').get(threadId);
|
|
687
|
+
if (!row) return null;
|
|
688
|
+
|
|
689
|
+
return {
|
|
690
|
+
...row,
|
|
691
|
+
mentions: parseJSON(row.mentions),
|
|
692
|
+
partialAnswers: parseJSON(row.partial_answers),
|
|
693
|
+
relatedConcepts: parseJSON(row.related_concepts),
|
|
694
|
+
};
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Get active threads for a learner
|
|
699
|
+
*/
|
|
700
|
+
export const getActiveThreads = (learnerId) => {
|
|
701
|
+
const rows = db.prepare(`
|
|
702
|
+
SELECT * FROM threads
|
|
703
|
+
WHERE learner_id = ? AND status = 'active'
|
|
704
|
+
ORDER BY last_touched DESC
|
|
705
|
+
`).all(learnerId);
|
|
706
|
+
|
|
707
|
+
return rows.map(row => ({
|
|
708
|
+
...row,
|
|
709
|
+
mentions: parseJSON(row.mentions),
|
|
710
|
+
partialAnswers: parseJSON(row.partial_answers),
|
|
711
|
+
relatedConcepts: parseJSON(row.related_concepts),
|
|
712
|
+
}));
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Update thread
|
|
717
|
+
*/
|
|
718
|
+
export const updateThread = (threadId, data) => {
|
|
719
|
+
const existing = getThread(threadId);
|
|
720
|
+
if (!existing) return null;
|
|
721
|
+
|
|
722
|
+
const updates = [];
|
|
723
|
+
const params = [];
|
|
724
|
+
|
|
725
|
+
if (data.status !== undefined) { updates.push('status = ?'); params.push(data.status); }
|
|
726
|
+
if (data.studentInterest !== undefined) { updates.push('student_interest = ?'); params.push(data.studentInterest); }
|
|
727
|
+
if (data.pedagogicalImportance !== undefined) { updates.push('pedagogical_importance = ?'); params.push(data.pedagogicalImportance); }
|
|
728
|
+
|
|
729
|
+
if (data.addMention) {
|
|
730
|
+
const mentions = [...existing.mentions, data.addMention];
|
|
731
|
+
updates.push('mentions = ?');
|
|
732
|
+
params.push(stringifyJSON(mentions));
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (data.addPartialAnswer) {
|
|
736
|
+
const partialAnswers = [...existing.partialAnswers, data.addPartialAnswer];
|
|
737
|
+
updates.push('partial_answers = ?');
|
|
738
|
+
params.push(stringifyJSON(partialAnswers));
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
updates.push('last_touched = datetime("now")');
|
|
742
|
+
updates.push('updated_at = datetime("now")');
|
|
743
|
+
params.push(threadId);
|
|
744
|
+
|
|
745
|
+
db.prepare(`
|
|
746
|
+
UPDATE threads
|
|
747
|
+
SET ${updates.join(', ')}
|
|
748
|
+
WHERE id = ?
|
|
749
|
+
`).run(...params);
|
|
750
|
+
|
|
751
|
+
return getThread(threadId);
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// ============================================================================
|
|
755
|
+
// Personal Definitions
|
|
756
|
+
// ============================================================================
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Get or create personal definition
|
|
760
|
+
*/
|
|
761
|
+
export const upsertPersonalDefinition = (learnerId, term, definition, confidence = 0.5) => {
|
|
762
|
+
const existing = db.prepare(`
|
|
763
|
+
SELECT * FROM personal_definitions
|
|
764
|
+
WHERE learner_id = ? AND term = ?
|
|
765
|
+
`).get(learnerId, term);
|
|
766
|
+
|
|
767
|
+
if (existing) {
|
|
768
|
+
// Track evolution
|
|
769
|
+
const evolution = parseJSON(existing.evolution);
|
|
770
|
+
evolution.push({
|
|
771
|
+
timestamp: new Date().toISOString(),
|
|
772
|
+
definition: existing.definition,
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
db.prepare(`
|
|
776
|
+
UPDATE personal_definitions
|
|
777
|
+
SET definition = ?, confidence = ?, evolution = ?, updated_at = datetime('now')
|
|
778
|
+
WHERE learner_id = ? AND term = ?
|
|
779
|
+
`).run(definition, confidence, stringifyJSON(evolution), learnerId, term);
|
|
780
|
+
} else {
|
|
781
|
+
const id = generateId();
|
|
782
|
+
db.prepare(`
|
|
783
|
+
INSERT INTO personal_definitions (id, learner_id, term, definition, confidence)
|
|
784
|
+
VALUES (?, ?, ?, ?, ?)
|
|
785
|
+
`).run(id, learnerId, term, definition, confidence);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return db.prepare(`
|
|
789
|
+
SELECT * FROM personal_definitions
|
|
790
|
+
WHERE learner_id = ? AND term = ?
|
|
791
|
+
`).get(learnerId, term);
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Get all personal definitions
|
|
796
|
+
*/
|
|
797
|
+
export const getPersonalDefinitions = (learnerId) => {
|
|
798
|
+
const rows = db.prepare(`
|
|
799
|
+
SELECT * FROM personal_definitions
|
|
800
|
+
WHERE learner_id = ?
|
|
801
|
+
ORDER BY updated_at DESC
|
|
802
|
+
`).all(learnerId);
|
|
803
|
+
|
|
804
|
+
return rows.map(row => ({
|
|
805
|
+
...row,
|
|
806
|
+
evolution: parseJSON(row.evolution),
|
|
807
|
+
}));
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
// ============================================================================
|
|
811
|
+
// Connections
|
|
812
|
+
// ============================================================================
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Create a connection
|
|
816
|
+
*/
|
|
817
|
+
export const createConnection = (data) => {
|
|
818
|
+
const id = generateId();
|
|
819
|
+
try {
|
|
820
|
+
db.prepare(`
|
|
821
|
+
INSERT INTO connections (
|
|
822
|
+
id, learner_id, concept_a, concept_b, relationship, source, strength
|
|
823
|
+
)
|
|
824
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
825
|
+
`).run(
|
|
826
|
+
id,
|
|
827
|
+
data.learnerId,
|
|
828
|
+
data.conceptA,
|
|
829
|
+
data.conceptB,
|
|
830
|
+
data.relationship,
|
|
831
|
+
data.source || 'student',
|
|
832
|
+
data.strength || 0.5
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
return db.prepare('SELECT * FROM connections WHERE id = ?').get(id);
|
|
836
|
+
} catch (e) {
|
|
837
|
+
// Unique constraint violation - connection already exists
|
|
838
|
+
return db.prepare(`
|
|
839
|
+
SELECT * FROM connections
|
|
840
|
+
WHERE learner_id = ? AND concept_a = ? AND concept_b = ?
|
|
841
|
+
`).get(data.learnerId, data.conceptA, data.conceptB);
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Get connections for a learner
|
|
847
|
+
*/
|
|
848
|
+
export const getConnections = (learnerId) => {
|
|
849
|
+
return db.prepare(`
|
|
850
|
+
SELECT * FROM connections
|
|
851
|
+
WHERE learner_id = ?
|
|
852
|
+
ORDER BY created_at DESC
|
|
853
|
+
`).all(learnerId);
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
// ============================================================================
|
|
857
|
+
// Preferences
|
|
858
|
+
// ============================================================================
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Get learner preferences
|
|
862
|
+
*/
|
|
863
|
+
export const getPreferences = (learnerId) => {
|
|
864
|
+
const row = db.prepare(`
|
|
865
|
+
SELECT * FROM learner_preferences
|
|
866
|
+
WHERE learner_id = ?
|
|
867
|
+
`).get(learnerId);
|
|
868
|
+
|
|
869
|
+
if (!row) return null;
|
|
870
|
+
|
|
871
|
+
return {
|
|
872
|
+
learnerId: row.learner_id,
|
|
873
|
+
prefersExamplesFirst: !!row.prefers_examples_first,
|
|
874
|
+
prefersSocraticMode: !!row.prefers_socratic_mode,
|
|
875
|
+
toleranceForChallenge: row.tolerance_for_challenge,
|
|
876
|
+
bestTimeOfDay: row.best_time_of_day,
|
|
877
|
+
typicalSessionLength: row.typical_session_length,
|
|
878
|
+
preferredPace: row.preferred_pace,
|
|
879
|
+
respondsToEncouragement: !!row.responds_to_encouragement,
|
|
880
|
+
needsNormalization: !!row.needs_normalization,
|
|
881
|
+
effectiveStrategies: parseJSON(row.effective_strategies),
|
|
882
|
+
preferredArchitecture: row.preferred_architecture,
|
|
883
|
+
updatedAt: row.updated_at,
|
|
884
|
+
};
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Update learner preferences
|
|
889
|
+
*/
|
|
890
|
+
export const updatePreferences = (learnerId, data) => {
|
|
891
|
+
// Ensure record exists
|
|
892
|
+
db.prepare(`
|
|
893
|
+
INSERT OR IGNORE INTO learner_preferences (learner_id)
|
|
894
|
+
VALUES (?)
|
|
895
|
+
`).run(learnerId);
|
|
896
|
+
|
|
897
|
+
const updates = [];
|
|
898
|
+
const params = [];
|
|
899
|
+
|
|
900
|
+
if (data.prefersExamplesFirst !== undefined) {
|
|
901
|
+
updates.push('prefers_examples_first = ?');
|
|
902
|
+
params.push(data.prefersExamplesFirst ? 1 : 0);
|
|
903
|
+
}
|
|
904
|
+
if (data.prefersSocraticMode !== undefined) {
|
|
905
|
+
updates.push('prefers_socratic_mode = ?');
|
|
906
|
+
params.push(data.prefersSocraticMode ? 1 : 0);
|
|
907
|
+
}
|
|
908
|
+
if (data.toleranceForChallenge !== undefined) {
|
|
909
|
+
updates.push('tolerance_for_challenge = ?');
|
|
910
|
+
params.push(data.toleranceForChallenge);
|
|
911
|
+
}
|
|
912
|
+
if (data.bestTimeOfDay !== undefined) {
|
|
913
|
+
updates.push('best_time_of_day = ?');
|
|
914
|
+
params.push(data.bestTimeOfDay);
|
|
915
|
+
}
|
|
916
|
+
if (data.typicalSessionLength !== undefined) {
|
|
917
|
+
updates.push('typical_session_length = ?');
|
|
918
|
+
params.push(data.typicalSessionLength);
|
|
919
|
+
}
|
|
920
|
+
if (data.preferredPace !== undefined) {
|
|
921
|
+
updates.push('preferred_pace = ?');
|
|
922
|
+
params.push(data.preferredPace);
|
|
923
|
+
}
|
|
924
|
+
if (data.respondsToEncouragement !== undefined) {
|
|
925
|
+
updates.push('responds_to_encouragement = ?');
|
|
926
|
+
params.push(data.respondsToEncouragement ? 1 : 0);
|
|
927
|
+
}
|
|
928
|
+
if (data.needsNormalization !== undefined) {
|
|
929
|
+
updates.push('needs_normalization = ?');
|
|
930
|
+
params.push(data.needsNormalization ? 1 : 0);
|
|
931
|
+
}
|
|
932
|
+
if (data.effectiveStrategies !== undefined) {
|
|
933
|
+
updates.push('effective_strategies = ?');
|
|
934
|
+
params.push(stringifyJSON(data.effectiveStrategies));
|
|
935
|
+
}
|
|
936
|
+
if (data.preferredArchitecture !== undefined) {
|
|
937
|
+
updates.push('preferred_architecture = ?');
|
|
938
|
+
params.push(data.preferredArchitecture);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (updates.length > 0) {
|
|
942
|
+
updates.push('updated_at = datetime("now")');
|
|
943
|
+
params.push(learnerId);
|
|
944
|
+
|
|
945
|
+
db.prepare(`
|
|
946
|
+
UPDATE learner_preferences
|
|
947
|
+
SET ${updates.join(', ')}
|
|
948
|
+
WHERE learner_id = ?
|
|
949
|
+
`).run(...params);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
return getPreferences(learnerId);
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
// ============================================================================
|
|
956
|
+
// Milestones
|
|
957
|
+
// ============================================================================
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Create a milestone
|
|
961
|
+
*/
|
|
962
|
+
export const createMilestone = (data) => {
|
|
963
|
+
const id = generateId();
|
|
964
|
+
db.prepare(`
|
|
965
|
+
INSERT INTO learning_milestones (
|
|
966
|
+
id, learner_id, type, title, description, concept_id, course_id
|
|
967
|
+
)
|
|
968
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
969
|
+
`).run(
|
|
970
|
+
id,
|
|
971
|
+
data.learnerId,
|
|
972
|
+
data.type,
|
|
973
|
+
data.title,
|
|
974
|
+
data.description || '',
|
|
975
|
+
data.conceptId || null,
|
|
976
|
+
data.courseId || null
|
|
977
|
+
);
|
|
978
|
+
|
|
979
|
+
return db.prepare('SELECT * FROM learning_milestones WHERE id = ?').get(id);
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Get unacknowledged milestones
|
|
984
|
+
*/
|
|
985
|
+
export const getUnacknowledgedMilestones = (learnerId) => {
|
|
986
|
+
return db.prepare(`
|
|
987
|
+
SELECT * FROM learning_milestones
|
|
988
|
+
WHERE learner_id = ? AND acknowledged = 0
|
|
989
|
+
ORDER BY achieved_at DESC
|
|
990
|
+
`).all(learnerId);
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Acknowledge milestone
|
|
995
|
+
*/
|
|
996
|
+
export const acknowledgeMilestone = (milestoneId) => {
|
|
997
|
+
db.prepare(`
|
|
998
|
+
UPDATE learning_milestones
|
|
999
|
+
SET acknowledged = 1
|
|
1000
|
+
WHERE id = ?
|
|
1001
|
+
`).run(milestoneId);
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
// ============================================================================
|
|
1005
|
+
// Cost Tracking
|
|
1006
|
+
// ============================================================================
|
|
1007
|
+
|
|
1008
|
+
/**
|
|
1009
|
+
* Log agent cost
|
|
1010
|
+
*/
|
|
1011
|
+
export const logAgentCost = (data) => {
|
|
1012
|
+
const id = generateId();
|
|
1013
|
+
db.prepare(`
|
|
1014
|
+
INSERT INTO agent_cost_log (
|
|
1015
|
+
id, learner_id, session_id, agent_role, architecture,
|
|
1016
|
+
input_tokens, output_tokens, latency_ms, model
|
|
1017
|
+
)
|
|
1018
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1019
|
+
`).run(
|
|
1020
|
+
id,
|
|
1021
|
+
data.learnerId,
|
|
1022
|
+
data.sessionId,
|
|
1023
|
+
data.agentRole,
|
|
1024
|
+
data.architecture,
|
|
1025
|
+
data.inputTokens || 0,
|
|
1026
|
+
data.outputTokens || 0,
|
|
1027
|
+
data.latencyMs || 0,
|
|
1028
|
+
data.model || ''
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
return id;
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Get cost statistics
|
|
1036
|
+
*/
|
|
1037
|
+
export const getCostStats = (learnerId, days = 30) => {
|
|
1038
|
+
const row = db.prepare(`
|
|
1039
|
+
SELECT
|
|
1040
|
+
COUNT(*) as total_calls,
|
|
1041
|
+
SUM(input_tokens) as total_input_tokens,
|
|
1042
|
+
SUM(output_tokens) as total_output_tokens,
|
|
1043
|
+
AVG(latency_ms) as avg_latency_ms,
|
|
1044
|
+
COUNT(DISTINCT session_id) as session_count
|
|
1045
|
+
FROM agent_cost_log
|
|
1046
|
+
WHERE learner_id = ?
|
|
1047
|
+
AND timestamp > datetime('now', '-' || ? || ' days')
|
|
1048
|
+
`).get(learnerId, days);
|
|
1049
|
+
|
|
1050
|
+
const byArchitecture = db.prepare(`
|
|
1051
|
+
SELECT
|
|
1052
|
+
architecture,
|
|
1053
|
+
COUNT(*) as calls,
|
|
1054
|
+
SUM(input_tokens + output_tokens) as total_tokens
|
|
1055
|
+
FROM agent_cost_log
|
|
1056
|
+
WHERE learner_id = ?
|
|
1057
|
+
AND timestamp > datetime('now', '-' || ? || ' days')
|
|
1058
|
+
GROUP BY architecture
|
|
1059
|
+
`).all(learnerId, days);
|
|
1060
|
+
|
|
1061
|
+
return { ...row, byArchitecture };
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
// ============================================================================
|
|
1065
|
+
// Context Injection
|
|
1066
|
+
// ============================================================================
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Build context for LLM injection
|
|
1070
|
+
* Returns a structured summary of the learner's history for system prompts
|
|
1071
|
+
*/
|
|
1072
|
+
export const buildContextInjection = (learnerId, options = {}) => {
|
|
1073
|
+
const {
|
|
1074
|
+
maxSessionSummaries = 3,
|
|
1075
|
+
maxEpisodes = 5,
|
|
1076
|
+
maxThreads = 3,
|
|
1077
|
+
includePreferences = true,
|
|
1078
|
+
} = options;
|
|
1079
|
+
|
|
1080
|
+
// Get recent session summaries
|
|
1081
|
+
const recentSessions = getRecentSessionSummaries(learnerId, maxSessionSummaries);
|
|
1082
|
+
|
|
1083
|
+
// Get active threads
|
|
1084
|
+
const activeThreads = getActiveThreads(learnerId).slice(0, maxThreads);
|
|
1085
|
+
|
|
1086
|
+
// Get concepts due for review
|
|
1087
|
+
const dueForReview = getConceptsDueForReview(learnerId, 3);
|
|
1088
|
+
|
|
1089
|
+
// Get important episodes
|
|
1090
|
+
const recentEpisodes = getImportantEpisodes(learnerId, 0.6, maxEpisodes);
|
|
1091
|
+
|
|
1092
|
+
// Get preferences
|
|
1093
|
+
const preferences = includePreferences ? getPreferences(learnerId) : null;
|
|
1094
|
+
|
|
1095
|
+
// Build narrative summary
|
|
1096
|
+
let narrative = '';
|
|
1097
|
+
|
|
1098
|
+
if (recentSessions.length > 0) {
|
|
1099
|
+
const lastSession = recentSessions[0];
|
|
1100
|
+
narrative += `Last session: ${lastSession.narrative_summary || 'No summary available'}. `;
|
|
1101
|
+
|
|
1102
|
+
if (lastSession.unresolvedQuestions?.length > 0) {
|
|
1103
|
+
narrative += `Unresolved: "${lastSession.unresolvedQuestions[0]}". `;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (activeThreads.length > 0) {
|
|
1108
|
+
narrative += `Active questions: ${activeThreads.map(t => t.question).join('; ')}. `;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (dueForReview.length > 0) {
|
|
1112
|
+
narrative += `Concepts due for review: ${dueForReview.map(c => c.label).join(', ')}. `;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
if (preferences) {
|
|
1116
|
+
const style = [];
|
|
1117
|
+
if (preferences.prefersExamplesFirst) style.push('prefers examples before abstractions');
|
|
1118
|
+
if (preferences.prefersSocraticMode) style.push('responds well to Socratic questioning');
|
|
1119
|
+
if (preferences.needsNormalization) style.push('benefits from hearing that difficulty is normal');
|
|
1120
|
+
if (style.length > 0) {
|
|
1121
|
+
narrative += `Learning style: ${style.join(', ')}. `;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Rough token estimate (4 chars per token average)
|
|
1126
|
+
const tokenCount = Math.ceil(narrative.length / 4);
|
|
1127
|
+
|
|
1128
|
+
return {
|
|
1129
|
+
narrativeSummary: narrative.trim(),
|
|
1130
|
+
recentSessions,
|
|
1131
|
+
activeThreads,
|
|
1132
|
+
dueForReview,
|
|
1133
|
+
recentEpisodes,
|
|
1134
|
+
preferences,
|
|
1135
|
+
tokenCount,
|
|
1136
|
+
};
|
|
1137
|
+
};
|
|
1138
|
+
|
|
1139
|
+
// ============================================================================
|
|
1140
|
+
// Full Knowledge Base Export
|
|
1141
|
+
// ============================================================================
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Get complete learner knowledge base
|
|
1145
|
+
* Useful for debugging or data export
|
|
1146
|
+
*/
|
|
1147
|
+
export const getFullKnowledgeBase = (learnerId) => {
|
|
1148
|
+
getOrCreateLearnerMemory(learnerId);
|
|
1149
|
+
|
|
1150
|
+
return {
|
|
1151
|
+
learnerId,
|
|
1152
|
+
concepts: getAllConceptStates(learnerId),
|
|
1153
|
+
episodes: getRecentEpisodes(learnerId, 100),
|
|
1154
|
+
sessions: getRecentSessionSummaries(learnerId, 20),
|
|
1155
|
+
activeThreads: getActiveThreads(learnerId),
|
|
1156
|
+
lexicon: getPersonalDefinitions(learnerId),
|
|
1157
|
+
connections: getConnections(learnerId),
|
|
1158
|
+
preferences: getPreferences(learnerId),
|
|
1159
|
+
milestones: db.prepare(`
|
|
1160
|
+
SELECT * FROM learning_milestones
|
|
1161
|
+
WHERE learner_id = ?
|
|
1162
|
+
ORDER BY achieved_at DESC
|
|
1163
|
+
`).all(learnerId),
|
|
1164
|
+
costStats: getCostStats(learnerId),
|
|
1165
|
+
};
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
// ============================================================================
|
|
1169
|
+
// Health Check
|
|
1170
|
+
// ============================================================================
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Check database health without creating records
|
|
1174
|
+
* @returns {boolean} - True if database is accessible
|
|
1175
|
+
*/
|
|
1176
|
+
export const checkDatabaseHealth = () => {
|
|
1177
|
+
try {
|
|
1178
|
+
// Simple query to check table existence and connection
|
|
1179
|
+
const result = db.prepare(`
|
|
1180
|
+
SELECT name FROM sqlite_master
|
|
1181
|
+
WHERE type='table' AND name='learner_memory'
|
|
1182
|
+
`).get();
|
|
1183
|
+
return !!result;
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
console.error('[LearnerMemory] Database health check failed:', error);
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
export default {
|
|
1191
|
+
getOrCreateLearnerMemory,
|
|
1192
|
+
updateLastSession,
|
|
1193
|
+
getConceptState,
|
|
1194
|
+
getAllConceptStates,
|
|
1195
|
+
getConceptsDueForReview,
|
|
1196
|
+
upsertConceptState,
|
|
1197
|
+
createEpisode,
|
|
1198
|
+
getRecentEpisodes,
|
|
1199
|
+
getEpisodesByType,
|
|
1200
|
+
getImportantEpisodes,
|
|
1201
|
+
incrementEpisodeRetrieval,
|
|
1202
|
+
decayEpisodeImportance,
|
|
1203
|
+
createSessionSummary,
|
|
1204
|
+
getSessionSummary,
|
|
1205
|
+
getRecentSessionSummaries,
|
|
1206
|
+
createThread,
|
|
1207
|
+
getThread,
|
|
1208
|
+
getActiveThreads,
|
|
1209
|
+
updateThread,
|
|
1210
|
+
upsertPersonalDefinition,
|
|
1211
|
+
getPersonalDefinitions,
|
|
1212
|
+
createConnection,
|
|
1213
|
+
getConnections,
|
|
1214
|
+
getPreferences,
|
|
1215
|
+
updatePreferences,
|
|
1216
|
+
createMilestone,
|
|
1217
|
+
getUnacknowledgedMilestones,
|
|
1218
|
+
acknowledgeMilestone,
|
|
1219
|
+
logAgentCost,
|
|
1220
|
+
getCostStats,
|
|
1221
|
+
buildContextInjection,
|
|
1222
|
+
getFullKnowledgeBase,
|
|
1223
|
+
checkDatabaseHealth,
|
|
1224
|
+
// Aliases for consistent naming
|
|
1225
|
+
getLearnerPreferences: getPreferences,
|
|
1226
|
+
upsertLearnerPreferences: updatePreferences,
|
|
1227
|
+
};
|