@hbarefoot/engram 1.0.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/LICENSE +21 -0
- package/README.md +535 -0
- package/bin/engram.js +421 -0
- package/dashboard/dist/assets/index-BHkLa5w_.css +1 -0
- package/dashboard/dist/assets/index-D9QR_Cnu.js +45 -0
- package/dashboard/dist/index.html +14 -0
- package/dashboard/package.json +21 -0
- package/package.json +76 -0
- package/src/config/index.js +150 -0
- package/src/embed/index.js +249 -0
- package/src/export/static.js +396 -0
- package/src/extract/rules.js +233 -0
- package/src/extract/secrets.js +114 -0
- package/src/index.js +54 -0
- package/src/memory/consolidate.js +420 -0
- package/src/memory/context.js +346 -0
- package/src/memory/feedback.js +197 -0
- package/src/memory/recall.js +350 -0
- package/src/memory/store.js +626 -0
- package/src/server/mcp.js +668 -0
- package/src/server/rest.js +499 -0
- package/src/utils/id.js +9 -0
- package/src/utils/logger.js +79 -0
- package/src/utils/time.js +296 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context generation for AI agents
|
|
3
|
+
* Generates pre-formatted context blocks from memories
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { listMemories, getMemoriesWithEmbeddings, updateAccessStats } from './store.js';
|
|
7
|
+
import { generateEmbedding, cosineSimilarity } from '../embed/index.js';
|
|
8
|
+
import * as logger from '../utils/logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Estimate token count for text (rough approximation)
|
|
12
|
+
* @param {string} text - Text to estimate
|
|
13
|
+
* @returns {number} Estimated token count
|
|
14
|
+
*/
|
|
15
|
+
function estimateTokens(text) {
|
|
16
|
+
return Math.ceil(text.length / 4);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate context from memories
|
|
21
|
+
* @param {Database} db - SQLite database instance
|
|
22
|
+
* @param {Object} options - Context generation options
|
|
23
|
+
* @param {string} [options.query] - Optional query to filter relevant memories
|
|
24
|
+
* @param {string} [options.namespace='default'] - Namespace to pull from
|
|
25
|
+
* @param {number} [options.limit=10] - Maximum memories to include
|
|
26
|
+
* @param {string} [options.format='markdown'] - Output format
|
|
27
|
+
* @param {boolean} [options.include_metadata=false] - Include memory IDs and scores
|
|
28
|
+
* @param {string[]} [options.categories] - Filter by categories
|
|
29
|
+
* @param {number} [options.max_tokens=1000] - Token budget
|
|
30
|
+
* @param {string} modelsPath - Path to embedding models
|
|
31
|
+
* @returns {Promise<Object>} Context object with content and metadata
|
|
32
|
+
*/
|
|
33
|
+
export async function generateContext(db, options = {}, modelsPath) {
|
|
34
|
+
const {
|
|
35
|
+
query,
|
|
36
|
+
namespace = 'default',
|
|
37
|
+
limit = 10,
|
|
38
|
+
format = 'markdown',
|
|
39
|
+
include_metadata = false,
|
|
40
|
+
categories,
|
|
41
|
+
max_tokens = 1000
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
logger.info('Generating context', { namespace, limit, format, max_tokens });
|
|
45
|
+
|
|
46
|
+
let memories = [];
|
|
47
|
+
|
|
48
|
+
if (query) {
|
|
49
|
+
// Query-based context: use semantic search
|
|
50
|
+
memories = await getRelevantMemories(db, query, namespace, categories, modelsPath);
|
|
51
|
+
} else {
|
|
52
|
+
// No query: get top memories by access frequency and recency
|
|
53
|
+
memories = getTopMemories(db, namespace, categories);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Limit to max count
|
|
57
|
+
memories = memories.slice(0, Math.min(limit, 25));
|
|
58
|
+
|
|
59
|
+
// Sort by category for grouping, then by score/relevance
|
|
60
|
+
memories = sortMemoriesForContext(memories);
|
|
61
|
+
|
|
62
|
+
// Truncate to fit token budget
|
|
63
|
+
memories = truncateToTokenBudget(memories, max_tokens);
|
|
64
|
+
|
|
65
|
+
// Update access stats
|
|
66
|
+
if (memories.length > 0) {
|
|
67
|
+
updateAccessStats(db, memories.map(m => m.id));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Generate output in requested format
|
|
71
|
+
const content = formatContext(memories, format, include_metadata, namespace);
|
|
72
|
+
|
|
73
|
+
logger.info('Context generated', {
|
|
74
|
+
memories: memories.length,
|
|
75
|
+
format,
|
|
76
|
+
estimatedTokens: estimateTokens(content)
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
content,
|
|
81
|
+
metadata: {
|
|
82
|
+
namespace,
|
|
83
|
+
count: memories.length,
|
|
84
|
+
format,
|
|
85
|
+
categories: [...new Set(memories.map(m => m.category))],
|
|
86
|
+
estimatedTokens: estimateTokens(content),
|
|
87
|
+
generatedAt: new Date().toISOString()
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get relevant memories using semantic search
|
|
94
|
+
*/
|
|
95
|
+
async function getRelevantMemories(db, query, namespace, categories, modelsPath) {
|
|
96
|
+
try {
|
|
97
|
+
const queryEmbedding = await generateEmbedding(query, modelsPath);
|
|
98
|
+
let memories = getMemoriesWithEmbeddings(db, namespace);
|
|
99
|
+
|
|
100
|
+
// Filter by categories if specified
|
|
101
|
+
if (categories && categories.length > 0) {
|
|
102
|
+
memories = memories.filter(m => categories.includes(m.category));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Filter out low feedback memories
|
|
106
|
+
memories = memories.filter(m => (m.feedback_score || 0) >= -0.3);
|
|
107
|
+
|
|
108
|
+
// Score and sort by similarity
|
|
109
|
+
const scored = memories.map(memory => {
|
|
110
|
+
const similarity = memory.embedding
|
|
111
|
+
? cosineSimilarity(queryEmbedding, memory.embedding)
|
|
112
|
+
: 0;
|
|
113
|
+
const relevanceScore = calculateRelevanceScore(memory, similarity);
|
|
114
|
+
return { ...memory, relevanceScore, similarity };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
scored.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
118
|
+
return scored;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.warn('Failed to generate embedding for context query, falling back to top memories', {
|
|
121
|
+
error: error.message
|
|
122
|
+
});
|
|
123
|
+
return getTopMemories(db, namespace, categories);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get top memories by access frequency and recency
|
|
129
|
+
*/
|
|
130
|
+
function getTopMemories(db, namespace, categories) {
|
|
131
|
+
let memories = listMemories(db, {
|
|
132
|
+
namespace,
|
|
133
|
+
limit: 100,
|
|
134
|
+
sort: 'access_count DESC, created_at DESC'
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Filter by categories if specified
|
|
138
|
+
if (categories && categories.length > 0) {
|
|
139
|
+
memories = memories.filter(m => categories.includes(m.category));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Filter out low feedback memories
|
|
143
|
+
memories = memories.filter(m => (m.feedback_score || 0) >= -0.3);
|
|
144
|
+
|
|
145
|
+
// Score memories
|
|
146
|
+
const scored = memories.map(memory => {
|
|
147
|
+
const relevanceScore = calculateRelevanceScore(memory, 0);
|
|
148
|
+
return { ...memory, relevanceScore };
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
scored.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
152
|
+
return scored;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Calculate relevance score for context inclusion
|
|
157
|
+
*/
|
|
158
|
+
function calculateRelevanceScore(memory, similarity = 0) {
|
|
159
|
+
const confidence = memory.confidence || 0.8;
|
|
160
|
+
const accessScore = Math.min(memory.access_count / 10, 1);
|
|
161
|
+
const recencyScore = calculateRecency(memory);
|
|
162
|
+
const feedbackScore = ((memory.feedback_score || 0) + 1) / 2;
|
|
163
|
+
|
|
164
|
+
// Weight components
|
|
165
|
+
return (similarity * 0.4) + (confidence * 0.25) + (accessScore * 0.15) +
|
|
166
|
+
(recencyScore * 0.1) + (feedbackScore * 0.1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Calculate recency score
|
|
171
|
+
*/
|
|
172
|
+
function calculateRecency(memory) {
|
|
173
|
+
const now = Date.now();
|
|
174
|
+
const lastAccessed = memory.last_accessed || memory.created_at;
|
|
175
|
+
const daysSince = (now - lastAccessed) / (1000 * 60 * 60 * 24);
|
|
176
|
+
return 1 / (1 + daysSince * 0.01);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Sort memories for context (group by category)
|
|
181
|
+
*/
|
|
182
|
+
function sortMemoriesForContext(memories) {
|
|
183
|
+
// Priority order for categories
|
|
184
|
+
const categoryOrder = ['preference', 'fact', 'pattern', 'decision', 'outcome'];
|
|
185
|
+
|
|
186
|
+
return memories.sort((a, b) => {
|
|
187
|
+
const orderA = categoryOrder.indexOf(a.category);
|
|
188
|
+
const orderB = categoryOrder.indexOf(b.category);
|
|
189
|
+
if (orderA !== orderB) {
|
|
190
|
+
return orderA - orderB;
|
|
191
|
+
}
|
|
192
|
+
// Within same category, sort by relevance score
|
|
193
|
+
return (b.relevanceScore || 0) - (a.relevanceScore || 0);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Truncate memories to fit token budget
|
|
199
|
+
*/
|
|
200
|
+
function truncateToTokenBudget(memories, maxTokens) {
|
|
201
|
+
let totalTokens = 100; // Reserve for headers/formatting
|
|
202
|
+
const selected = [];
|
|
203
|
+
|
|
204
|
+
for (const memory of memories) {
|
|
205
|
+
const memoryTokens = estimateTokens(memory.content) + 20; // +20 for metadata
|
|
206
|
+
if (totalTokens + memoryTokens <= maxTokens) {
|
|
207
|
+
selected.push(memory);
|
|
208
|
+
totalTokens += memoryTokens;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return selected;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Format context in requested format
|
|
217
|
+
*/
|
|
218
|
+
function formatContext(memories, format, includeMetadata, namespace) {
|
|
219
|
+
switch (format) {
|
|
220
|
+
case 'markdown':
|
|
221
|
+
return formatMarkdown(memories, includeMetadata, namespace);
|
|
222
|
+
case 'xml':
|
|
223
|
+
return formatXML(memories, includeMetadata, namespace);
|
|
224
|
+
case 'json':
|
|
225
|
+
return formatJSON(memories, namespace);
|
|
226
|
+
case 'plain':
|
|
227
|
+
return formatPlain(memories);
|
|
228
|
+
default:
|
|
229
|
+
return formatMarkdown(memories, includeMetadata, namespace);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Format as Markdown
|
|
235
|
+
*/
|
|
236
|
+
function formatMarkdown(memories, includeMetadata, namespace) {
|
|
237
|
+
if (memories.length === 0) {
|
|
238
|
+
return '## User Context from Memory\n\nNo memories found.';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const lines = ['## User Context from Memory\n'];
|
|
242
|
+
|
|
243
|
+
// Group by category
|
|
244
|
+
const groups = groupByCategory(memories);
|
|
245
|
+
|
|
246
|
+
for (const [category, categoryMemories] of Object.entries(groups)) {
|
|
247
|
+
const categoryTitle = category.charAt(0).toUpperCase() + category.slice(1) + 's';
|
|
248
|
+
lines.push(`**${categoryTitle}:**`);
|
|
249
|
+
|
|
250
|
+
for (const memory of categoryMemories) {
|
|
251
|
+
if (includeMetadata) {
|
|
252
|
+
lines.push(`- ${memory.content} *(id: ${memory.id.substring(0, 8)}, confidence: ${memory.confidence.toFixed(2)})*`);
|
|
253
|
+
} else {
|
|
254
|
+
lines.push(`- ${memory.content}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
lines.push('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
lines.push('---');
|
|
261
|
+
lines.push(`*${memories.length} memories loaded from namespace "${namespace}" | Generated by Engram*`);
|
|
262
|
+
|
|
263
|
+
return lines.join('\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Format as XML
|
|
268
|
+
*/
|
|
269
|
+
function formatXML(memories, includeMetadata, namespace) {
|
|
270
|
+
if (memories.length === 0) {
|
|
271
|
+
return `<engram_context namespace="${namespace}" count="0" />\n`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const lines = [`<engram_context namespace="${namespace}" count="${memories.length}">`];
|
|
275
|
+
|
|
276
|
+
// Group by category
|
|
277
|
+
const groups = groupByCategory(memories);
|
|
278
|
+
|
|
279
|
+
for (const [category, categoryMemories] of Object.entries(groups)) {
|
|
280
|
+
lines.push(` <${category}s>`);
|
|
281
|
+
|
|
282
|
+
for (const memory of categoryMemories) {
|
|
283
|
+
if (includeMetadata) {
|
|
284
|
+
lines.push(` <memory id="${memory.id}" confidence="${memory.confidence.toFixed(2)}">${escapeXML(memory.content)}</memory>`);
|
|
285
|
+
} else {
|
|
286
|
+
lines.push(` <memory>${escapeXML(memory.content)}</memory>`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
lines.push(` </${category}s>`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
lines.push('</engram_context>');
|
|
294
|
+
|
|
295
|
+
return lines.join('\n');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Format as JSON
|
|
300
|
+
*/
|
|
301
|
+
function formatJSON(memories, namespace) {
|
|
302
|
+
const groups = groupByCategory(memories);
|
|
303
|
+
|
|
304
|
+
return JSON.stringify({
|
|
305
|
+
namespace,
|
|
306
|
+
count: memories.length,
|
|
307
|
+
memories: groups,
|
|
308
|
+
generated_at: new Date().toISOString()
|
|
309
|
+
}, null, 2);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Format as plain text
|
|
314
|
+
*/
|
|
315
|
+
function formatPlain(memories) {
|
|
316
|
+
return memories.map(m => m.content).join('. ');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Group memories by category
|
|
321
|
+
*/
|
|
322
|
+
function groupByCategory(memories) {
|
|
323
|
+
const groups = {};
|
|
324
|
+
|
|
325
|
+
for (const memory of memories) {
|
|
326
|
+
const cat = memory.category || 'fact';
|
|
327
|
+
if (!groups[cat]) {
|
|
328
|
+
groups[cat] = [];
|
|
329
|
+
}
|
|
330
|
+
groups[cat].push(memory);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return groups;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Escape XML special characters
|
|
338
|
+
*/
|
|
339
|
+
function escapeXML(text) {
|
|
340
|
+
return text
|
|
341
|
+
.replace(/&/g, '&')
|
|
342
|
+
.replace(/</g, '<')
|
|
343
|
+
.replace(/>/g, '>')
|
|
344
|
+
.replace(/"/g, '"')
|
|
345
|
+
.replace(/'/g, ''');
|
|
346
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory feedback system for confidence adjustments
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { generateId } from '../utils/id.js';
|
|
6
|
+
import * as logger from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Record feedback for a memory
|
|
10
|
+
* @param {Database} db - SQLite database instance
|
|
11
|
+
* @param {string} memoryId - ID of the memory
|
|
12
|
+
* @param {boolean} helpful - Whether the memory was helpful
|
|
13
|
+
* @param {string} [context] - Optional context for the feedback
|
|
14
|
+
* @returns {Object} Feedback result with updated scores
|
|
15
|
+
*/
|
|
16
|
+
export function recordFeedback(db, memoryId, helpful, context = null) {
|
|
17
|
+
const id = generateId();
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
|
|
20
|
+
// Insert feedback record
|
|
21
|
+
const insertStmt = db.prepare(`
|
|
22
|
+
INSERT INTO memory_feedback (id, memory_id, helpful, context, created_at)
|
|
23
|
+
VALUES (?, ?, ?, ?, ?)
|
|
24
|
+
`);
|
|
25
|
+
|
|
26
|
+
insertStmt.run(id, memoryId, helpful ? 1 : 0, context, now);
|
|
27
|
+
|
|
28
|
+
logger.debug('Feedback recorded', { feedbackId: id, memoryId, helpful });
|
|
29
|
+
|
|
30
|
+
// Recalculate feedback_score for the memory
|
|
31
|
+
const stats = db.prepare(`
|
|
32
|
+
SELECT
|
|
33
|
+
COUNT(*) as total,
|
|
34
|
+
SUM(CASE WHEN helpful = 1 THEN 1 ELSE 0 END) as helpful_count
|
|
35
|
+
FROM memory_feedback
|
|
36
|
+
WHERE memory_id = ?
|
|
37
|
+
`).get(memoryId);
|
|
38
|
+
|
|
39
|
+
// Calculate feedback_score: ranges from -1.0 (all unhelpful) to +1.0 (all helpful)
|
|
40
|
+
const feedbackScore = stats.total > 0
|
|
41
|
+
? (2 * stats.helpful_count - stats.total) / stats.total
|
|
42
|
+
: 0;
|
|
43
|
+
|
|
44
|
+
// Update the memory's feedback_score
|
|
45
|
+
db.prepare(`UPDATE memories SET feedback_score = ? WHERE id = ?`)
|
|
46
|
+
.run(feedbackScore, memoryId);
|
|
47
|
+
|
|
48
|
+
logger.info('Memory feedback score updated', {
|
|
49
|
+
memoryId,
|
|
50
|
+
feedbackScore,
|
|
51
|
+
totalFeedback: stats.total,
|
|
52
|
+
helpfulCount: stats.helpful_count
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Apply automatic confidence adjustment if threshold met
|
|
56
|
+
const adjustmentResult = applyConfidenceAdjustment(db, memoryId, feedbackScore, stats.total);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
feedbackId: id,
|
|
60
|
+
memoryId,
|
|
61
|
+
feedbackScore,
|
|
62
|
+
feedbackCount: stats.total,
|
|
63
|
+
helpfulCount: stats.helpful_count,
|
|
64
|
+
unhelpfulCount: stats.total - stats.helpful_count,
|
|
65
|
+
confidenceAdjusted: adjustmentResult.adjusted,
|
|
66
|
+
newConfidence: adjustmentResult.confidence
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Apply automatic confidence adjustment based on feedback
|
|
72
|
+
* @param {Database} db - SQLite database instance
|
|
73
|
+
* @param {string} memoryId - ID of the memory
|
|
74
|
+
* @param {number} feedbackScore - Current feedback score (-1 to 1)
|
|
75
|
+
* @param {number} feedbackCount - Total number of feedback events
|
|
76
|
+
* @returns {Object} Adjustment result
|
|
77
|
+
*/
|
|
78
|
+
function applyConfidenceAdjustment(db, memoryId, feedbackScore, feedbackCount) {
|
|
79
|
+
// Only adjust after minimum feedback threshold
|
|
80
|
+
const MIN_FEEDBACK_FOR_ADJUSTMENT = 5;
|
|
81
|
+
|
|
82
|
+
if (feedbackCount < MIN_FEEDBACK_FOR_ADJUSTMENT) {
|
|
83
|
+
const memory = db.prepare('SELECT confidence FROM memories WHERE id = ?').get(memoryId);
|
|
84
|
+
return { adjusted: false, confidence: memory?.confidence || 0.8 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const memory = db.prepare('SELECT confidence FROM memories WHERE id = ?').get(memoryId);
|
|
88
|
+
if (!memory) {
|
|
89
|
+
return { adjusted: false, confidence: 0.8 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let newConfidence = memory.confidence;
|
|
93
|
+
|
|
94
|
+
// Decrease confidence for consistently unhelpful memories
|
|
95
|
+
if (feedbackScore < -0.5) {
|
|
96
|
+
newConfidence = Math.max(0.1, memory.confidence - 0.1);
|
|
97
|
+
logger.info('Confidence decreased due to negative feedback', {
|
|
98
|
+
memoryId,
|
|
99
|
+
oldConfidence: memory.confidence,
|
|
100
|
+
newConfidence,
|
|
101
|
+
feedbackScore
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Increase confidence for consistently helpful memories
|
|
105
|
+
else if (feedbackScore > 0.5) {
|
|
106
|
+
newConfidence = Math.min(1.0, memory.confidence + 0.05);
|
|
107
|
+
logger.info('Confidence increased due to positive feedback', {
|
|
108
|
+
memoryId,
|
|
109
|
+
oldConfidence: memory.confidence,
|
|
110
|
+
newConfidence,
|
|
111
|
+
feedbackScore
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (newConfidence !== memory.confidence) {
|
|
116
|
+
db.prepare('UPDATE memories SET confidence = ? WHERE id = ?')
|
|
117
|
+
.run(newConfidence, memoryId);
|
|
118
|
+
return { adjusted: true, confidence: newConfidence };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { adjusted: false, confidence: memory.confidence };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get feedback history for a memory
|
|
126
|
+
* @param {Database} db - SQLite database instance
|
|
127
|
+
* @param {string} memoryId - ID of the memory
|
|
128
|
+
* @param {number} [limit=10] - Maximum feedback records to return
|
|
129
|
+
* @returns {Object[]} Array of feedback records
|
|
130
|
+
*/
|
|
131
|
+
export function getFeedbackHistory(db, memoryId, limit = 10) {
|
|
132
|
+
const stmt = db.prepare(`
|
|
133
|
+
SELECT id, helpful, context, created_at
|
|
134
|
+
FROM memory_feedback
|
|
135
|
+
WHERE memory_id = ?
|
|
136
|
+
ORDER BY created_at DESC
|
|
137
|
+
LIMIT ?
|
|
138
|
+
`);
|
|
139
|
+
|
|
140
|
+
return stmt.all(memoryId, limit).map(row => ({
|
|
141
|
+
id: row.id,
|
|
142
|
+
helpful: row.helpful === 1,
|
|
143
|
+
context: row.context,
|
|
144
|
+
createdAt: row.created_at
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get feedback statistics for a memory
|
|
150
|
+
* @param {Database} db - SQLite database instance
|
|
151
|
+
* @param {string} memoryId - ID of the memory
|
|
152
|
+
* @returns {Object} Feedback statistics
|
|
153
|
+
*/
|
|
154
|
+
export function getFeedbackStats(db, memoryId) {
|
|
155
|
+
const stats = db.prepare(`
|
|
156
|
+
SELECT
|
|
157
|
+
COUNT(*) as total,
|
|
158
|
+
SUM(CASE WHEN helpful = 1 THEN 1 ELSE 0 END) as helpful_count,
|
|
159
|
+
MIN(created_at) as first_feedback,
|
|
160
|
+
MAX(created_at) as last_feedback
|
|
161
|
+
FROM memory_feedback
|
|
162
|
+
WHERE memory_id = ?
|
|
163
|
+
`).get(memoryId);
|
|
164
|
+
|
|
165
|
+
const memory = db.prepare('SELECT feedback_score, confidence FROM memories WHERE id = ?')
|
|
166
|
+
.get(memoryId);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
totalFeedback: stats.total || 0,
|
|
170
|
+
helpfulCount: stats.helpful_count || 0,
|
|
171
|
+
unhelpfulCount: (stats.total || 0) - (stats.helpful_count || 0),
|
|
172
|
+
feedbackScore: memory?.feedback_score || 0,
|
|
173
|
+
confidence: memory?.confidence || 0.8,
|
|
174
|
+
firstFeedbackAt: stats.first_feedback,
|
|
175
|
+
lastFeedbackAt: stats.last_feedback
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get all memories with low feedback scores
|
|
181
|
+
* @param {Database} db - SQLite database instance
|
|
182
|
+
* @param {number} [threshold=-0.3] - Feedback score threshold
|
|
183
|
+
* @param {number} [minFeedback=3] - Minimum feedback count
|
|
184
|
+
* @returns {Object[]} Array of memories with low scores
|
|
185
|
+
*/
|
|
186
|
+
export function getLowFeedbackMemories(db, threshold = -0.3, minFeedback = 3) {
|
|
187
|
+
const stmt = db.prepare(`
|
|
188
|
+
SELECT m.*,
|
|
189
|
+
(SELECT COUNT(*) FROM memory_feedback f WHERE f.memory_id = m.id) as feedback_count
|
|
190
|
+
FROM memories m
|
|
191
|
+
WHERE m.feedback_score <= ?
|
|
192
|
+
AND (SELECT COUNT(*) FROM memory_feedback f WHERE f.memory_id = m.id) >= ?
|
|
193
|
+
ORDER BY m.feedback_score ASC
|
|
194
|
+
`);
|
|
195
|
+
|
|
196
|
+
return stmt.all(threshold, minFeedback);
|
|
197
|
+
}
|