libre-webui 0.2.4
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 +201 -0
- package/README.md +204 -0
- package/backend/dist/db.d.ts +19 -0
- package/backend/dist/db.d.ts.map +1 -0
- package/backend/dist/db.js +355 -0
- package/backend/dist/db.js.map +1 -0
- package/backend/dist/env.d.ts +2 -0
- package/backend/dist/env.d.ts.map +1 -0
- package/backend/dist/env.js +22 -0
- package/backend/dist/env.js.map +1 -0
- package/backend/dist/index.d.ts +4 -0
- package/backend/dist/index.d.ts.map +1 -0
- package/backend/dist/index.js +751 -0
- package/backend/dist/index.js.map +1 -0
- package/backend/dist/middleware/auth.d.ts +18 -0
- package/backend/dist/middleware/auth.d.ts.map +1 -0
- package/backend/dist/middleware/auth.js +98 -0
- package/backend/dist/middleware/auth.js.map +1 -0
- package/backend/dist/middleware/index.d.ts +5 -0
- package/backend/dist/middleware/index.d.ts.map +1 -0
- package/backend/dist/middleware/index.js +62 -0
- package/backend/dist/middleware/index.js.map +1 -0
- package/backend/dist/models/personaModel.d.ts +37 -0
- package/backend/dist/models/personaModel.d.ts.map +1 -0
- package/backend/dist/models/personaModel.js +269 -0
- package/backend/dist/models/personaModel.js.map +1 -0
- package/backend/dist/models/userModel.d.ts +86 -0
- package/backend/dist/models/userModel.d.ts.map +1 -0
- package/backend/dist/models/userModel.js +212 -0
- package/backend/dist/models/userModel.js.map +1 -0
- package/backend/dist/routes/auth.d.ts +3 -0
- package/backend/dist/routes/auth.d.ts.map +1 -0
- package/backend/dist/routes/auth.js +389 -0
- package/backend/dist/routes/auth.js.map +1 -0
- package/backend/dist/routes/chat.d.ts +3 -0
- package/backend/dist/routes/chat.d.ts.map +1 -0
- package/backend/dist/routes/chat.js +767 -0
- package/backend/dist/routes/chat.js.map +1 -0
- package/backend/dist/routes/documents.d.ts +3 -0
- package/backend/dist/routes/documents.d.ts.map +1 -0
- package/backend/dist/routes/documents.js +244 -0
- package/backend/dist/routes/documents.js.map +1 -0
- package/backend/dist/routes/ollama.d.ts +3 -0
- package/backend/dist/routes/ollama.d.ts.map +1 -0
- package/backend/dist/routes/ollama.js +549 -0
- package/backend/dist/routes/ollama.js.map +1 -0
- package/backend/dist/routes/personas.d.ts +3 -0
- package/backend/dist/routes/personas.d.ts.map +1 -0
- package/backend/dist/routes/personas.js +505 -0
- package/backend/dist/routes/personas.js.map +1 -0
- package/backend/dist/routes/plugins.d.ts +3 -0
- package/backend/dist/routes/plugins.d.ts.map +1 -0
- package/backend/dist/routes/plugins.js +417 -0
- package/backend/dist/routes/plugins.js.map +1 -0
- package/backend/dist/routes/preferences.d.ts +3 -0
- package/backend/dist/routes/preferences.d.ts.map +1 -0
- package/backend/dist/routes/preferences.js +303 -0
- package/backend/dist/routes/preferences.js.map +1 -0
- package/backend/dist/routes/tts.d.ts +3 -0
- package/backend/dist/routes/tts.d.ts.map +1 -0
- package/backend/dist/routes/tts.js +304 -0
- package/backend/dist/routes/tts.js.map +1 -0
- package/backend/dist/routes/users.d.ts +3 -0
- package/backend/dist/routes/users.d.ts.map +1 -0
- package/backend/dist/routes/users.js +246 -0
- package/backend/dist/routes/users.js.map +1 -0
- package/backend/dist/services/authService.d.ts +51 -0
- package/backend/dist/services/authService.d.ts.map +1 -0
- package/backend/dist/services/authService.js +153 -0
- package/backend/dist/services/authService.js.map +1 -0
- package/backend/dist/services/chatService.d.ts +52 -0
- package/backend/dist/services/chatService.d.ts.map +1 -0
- package/backend/dist/services/chatService.js +645 -0
- package/backend/dist/services/chatService.js.map +1 -0
- package/backend/dist/services/documentService.d.ts +34 -0
- package/backend/dist/services/documentService.d.ts.map +1 -0
- package/backend/dist/services/documentService.js +428 -0
- package/backend/dist/services/documentService.js.map +1 -0
- package/backend/dist/services/encryptionService.d.ts +62 -0
- package/backend/dist/services/encryptionService.d.ts.map +1 -0
- package/backend/dist/services/encryptionService.js +284 -0
- package/backend/dist/services/encryptionService.js.map +1 -0
- package/backend/dist/services/memoryService.d.ts +140 -0
- package/backend/dist/services/memoryService.d.ts.map +1 -0
- package/backend/dist/services/memoryService.js +867 -0
- package/backend/dist/services/memoryService.js.map +1 -0
- package/backend/dist/services/mutationEngineService.d.ts +49 -0
- package/backend/dist/services/mutationEngineService.d.ts.map +1 -0
- package/backend/dist/services/mutationEngineService.js +432 -0
- package/backend/dist/services/mutationEngineService.js.map +1 -0
- package/backend/dist/services/ollamaService.d.ts +55 -0
- package/backend/dist/services/ollamaService.d.ts.map +1 -0
- package/backend/dist/services/ollamaService.js +450 -0
- package/backend/dist/services/ollamaService.js.map +1 -0
- package/backend/dist/services/personaService.d.ts +67 -0
- package/backend/dist/services/personaService.d.ts.map +1 -0
- package/backend/dist/services/personaService.js +373 -0
- package/backend/dist/services/personaService.js.map +1 -0
- package/backend/dist/services/pluginService.d.ts +42 -0
- package/backend/dist/services/pluginService.d.ts.map +1 -0
- package/backend/dist/services/pluginService.js +961 -0
- package/backend/dist/services/pluginService.js.map +1 -0
- package/backend/dist/services/preferencesService.d.ts +35 -0
- package/backend/dist/services/preferencesService.d.ts.map +1 -0
- package/backend/dist/services/preferencesService.js +255 -0
- package/backend/dist/services/preferencesService.js.map +1 -0
- package/backend/dist/services/simpleGitHubOAuth.d.ts +48 -0
- package/backend/dist/services/simpleGitHubOAuth.d.ts.map +1 -0
- package/backend/dist/services/simpleGitHubOAuth.js +203 -0
- package/backend/dist/services/simpleGitHubOAuth.js.map +1 -0
- package/backend/dist/services/simpleHuggingFaceOAuth.d.ts +43 -0
- package/backend/dist/services/simpleHuggingFaceOAuth.d.ts.map +1 -0
- package/backend/dist/services/simpleHuggingFaceOAuth.js +159 -0
- package/backend/dist/services/simpleHuggingFaceOAuth.js.map +1 -0
- package/backend/dist/services/userService.d.ts +1 -0
- package/backend/dist/services/userService.d.ts.map +1 -0
- package/backend/dist/services/userService.js +18 -0
- package/backend/dist/services/userService.js.map +1 -0
- package/backend/dist/storage.d.ts +55 -0
- package/backend/dist/storage.d.ts.map +1 -0
- package/backend/dist/storage.js +741 -0
- package/backend/dist/storage.js.map +1 -0
- package/backend/dist/test-encryption.d.ts +2 -0
- package/backend/dist/test-encryption.d.ts.map +1 -0
- package/backend/dist/test-encryption.js +64 -0
- package/backend/dist/test-encryption.js.map +1 -0
- package/backend/dist/types/index.d.ts +523 -0
- package/backend/dist/types/index.d.ts.map +1 -0
- package/backend/dist/types/index.js +31 -0
- package/backend/dist/types/index.js.map +1 -0
- package/backend/dist/utils/generationUtils.d.ts +10 -0
- package/backend/dist/utils/generationUtils.d.ts.map +1 -0
- package/backend/dist/utils/generationUtils.js +49 -0
- package/backend/dist/utils/generationUtils.js.map +1 -0
- package/backend/dist/utils/hash.d.ts +29 -0
- package/backend/dist/utils/hash.d.ts.map +1 -0
- package/backend/dist/utils/hash.js +73 -0
- package/backend/dist/utils/hash.js.map +1 -0
- package/backend/dist/utils/jwt.d.ts +37 -0
- package/backend/dist/utils/jwt.d.ts.map +1 -0
- package/backend/dist/utils/jwt.js +86 -0
- package/backend/dist/utils/jwt.js.map +1 -0
- package/bin/cli.js +150 -0
- package/electron/main.js +322 -0
- package/frontend/dist/_redirects +1 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-CRQkB7Wz.js +3 -0
- package/frontend/dist/css/index-B1OjddR-.css +1 -0
- package/frontend/dist/favicon-dark.png +0 -0
- package/frontend/dist/favicon-light.png +0 -0
- package/frontend/dist/index.html +23 -0
- package/frontend/dist/js/ArtifactContainer-c_oi7XMs.js +23 -0
- package/frontend/dist/js/ArtifactDemoPage-CdfwJVXu.js +98 -0
- package/frontend/dist/js/ChatPage-CyotkmS0.js +281 -0
- package/frontend/dist/js/ModelsPage-DNaziPHc.js +2 -0
- package/frontend/dist/js/PersonasPage-DcnbJf8Q.js +13 -0
- package/frontend/dist/js/UserManagementPage-DtTf92dS.js +1 -0
- package/frontend/dist/js/markdown-vendor-D-79K2xZ.js +22 -0
- package/frontend/dist/js/react-vendor-N--QU9DW.js +8 -0
- package/frontend/dist/js/router-vendor-B-t91v39.js +3 -0
- package/frontend/dist/js/ui-vendor-VxSCY_bv.js +177 -0
- package/frontend/dist/js/utils-vendor-DNzxLBGx.js +6 -0
- package/frontend/dist/logo-dark.png +0 -0
- package/frontend/dist/logo-light.png +0 -0
- package/frontend/dist/logo.svg +14 -0
- package/package.json +128 -0
- package/plugins/anthropic.json +25 -0
- package/plugins/elevenlabs.json +58 -0
- package/plugins/gemini.json +57 -0
- package/plugins/github.json +23 -0
- package/plugins/groq.json +25 -0
- package/plugins/mistral.json +73 -0
- package/plugins/openai-tts.json +38 -0
- package/plugins/openai.json +132 -0
- package/plugins/openrouter.json +353 -0
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Libre WebUI
|
|
3
|
+
* Copyright (C) 2025 Kroonen AI, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { getDatabaseSafe } from '../db.js';
|
|
18
|
+
import ollamaService from './ollamaService.js';
|
|
19
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
20
|
+
export class MemoryService {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.db = getDatabaseSafe();
|
|
23
|
+
this.embeddingModels = [
|
|
24
|
+
{
|
|
25
|
+
id: 'nomic-embed-text',
|
|
26
|
+
name: 'Nomic Embed Text',
|
|
27
|
+
description: 'High-quality text embeddings from Nomic AI',
|
|
28
|
+
provider: 'ollama',
|
|
29
|
+
dimensions: 768,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'bge-m3',
|
|
33
|
+
name: 'BGE-M3',
|
|
34
|
+
description: 'Multi-lingual and multi-granularity embedding model',
|
|
35
|
+
provider: 'ollama',
|
|
36
|
+
dimensions: 1024,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'text-embedding-3-large',
|
|
40
|
+
name: 'OpenAI Text Embedding 3 Large',
|
|
41
|
+
description: "OpenAI's largest text embedding model",
|
|
42
|
+
provider: 'openai',
|
|
43
|
+
dimensions: 3072,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'text-embedding-3-small',
|
|
47
|
+
name: 'OpenAI Text Embedding 3 Small',
|
|
48
|
+
description: "OpenAI's smaller, faster text embedding model",
|
|
49
|
+
provider: 'openai',
|
|
50
|
+
dimensions: 1536,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'e5-large-v2',
|
|
54
|
+
name: 'E5 Large v2',
|
|
55
|
+
description: 'Microsoft E5 large text embedding model',
|
|
56
|
+
provider: 'sentence-transformers',
|
|
57
|
+
dimensions: 1024,
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
this.initializeTables();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Ensure database is available
|
|
64
|
+
*/
|
|
65
|
+
ensureDatabase() {
|
|
66
|
+
if (!this.db) {
|
|
67
|
+
throw new Error('Database not available');
|
|
68
|
+
}
|
|
69
|
+
return this.db;
|
|
70
|
+
}
|
|
71
|
+
initializeTables() {
|
|
72
|
+
if (!this.db) {
|
|
73
|
+
console.warn('MemoryService: Database not available, skipping table initialization');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Persona memories table - create with basic schema first
|
|
77
|
+
this.db.exec(`
|
|
78
|
+
CREATE TABLE IF NOT EXISTS persona_memories (
|
|
79
|
+
id TEXT PRIMARY KEY,
|
|
80
|
+
user_id TEXT NOT NULL,
|
|
81
|
+
persona_id TEXT NOT NULL,
|
|
82
|
+
content TEXT NOT NULL,
|
|
83
|
+
embedding BLOB, -- Stored as binary for efficiency
|
|
84
|
+
timestamp INTEGER NOT NULL,
|
|
85
|
+
context TEXT,
|
|
86
|
+
importance_score REAL DEFAULT 0.5,
|
|
87
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
88
|
+
FOREIGN KEY (persona_id) REFERENCES personas(id) ON DELETE CASCADE
|
|
89
|
+
)
|
|
90
|
+
`);
|
|
91
|
+
// Create basic indexes first (on columns that always exist)
|
|
92
|
+
this.db.exec(`
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_persona_memories_user_persona ON persona_memories(user_id, persona_id);
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_persona_memories_timestamp ON persona_memories(timestamp);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_persona_memories_importance ON persona_memories(importance_score);
|
|
96
|
+
`);
|
|
97
|
+
// Add new columns if they don't exist (migration for existing databases)
|
|
98
|
+
// This MUST happen before creating indexes on new columns
|
|
99
|
+
this.migrateDatabase();
|
|
100
|
+
// Now create indexes on the new columns (after migration ensures they exist)
|
|
101
|
+
try {
|
|
102
|
+
this.db.exec(`
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_persona_memories_type ON persona_memories(memory_type);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_persona_memories_last_accessed ON persona_memories(last_accessed);
|
|
105
|
+
`);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
// Indexes may already exist, ignore
|
|
109
|
+
console.warn('[MemoryService] Index creation:', error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Migrate database to add new columns for existing tables
|
|
114
|
+
*/
|
|
115
|
+
migrateDatabase() {
|
|
116
|
+
if (!this.db)
|
|
117
|
+
return;
|
|
118
|
+
try {
|
|
119
|
+
// Check if new columns exist and add them if not
|
|
120
|
+
const columns = [
|
|
121
|
+
'memory_type',
|
|
122
|
+
'access_count',
|
|
123
|
+
'last_accessed',
|
|
124
|
+
'decay_factor',
|
|
125
|
+
'consolidated_from',
|
|
126
|
+
];
|
|
127
|
+
const defaults = {
|
|
128
|
+
memory_type: "'general'",
|
|
129
|
+
access_count: '0',
|
|
130
|
+
last_accessed: 'NULL',
|
|
131
|
+
decay_factor: '1.0',
|
|
132
|
+
consolidated_from: 'NULL',
|
|
133
|
+
};
|
|
134
|
+
for (const column of columns) {
|
|
135
|
+
try {
|
|
136
|
+
this.db.exec(`ALTER TABLE persona_memories ADD COLUMN ${column} ${this.getColumnType(column)} DEFAULT ${defaults[column]}`);
|
|
137
|
+
console.log(`[MemoryService] Added column ${column} to persona_memories`);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Column likely already exists, ignore
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.warn('[MemoryService] Migration check:', error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get SQLite column type for a given column name
|
|
150
|
+
*/
|
|
151
|
+
getColumnType(column) {
|
|
152
|
+
const types = {
|
|
153
|
+
memory_type: 'TEXT',
|
|
154
|
+
access_count: 'INTEGER',
|
|
155
|
+
last_accessed: 'INTEGER',
|
|
156
|
+
decay_factor: 'REAL',
|
|
157
|
+
consolidated_from: 'TEXT',
|
|
158
|
+
};
|
|
159
|
+
return types[column] || 'TEXT';
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Classify memory content into a type
|
|
163
|
+
*/
|
|
164
|
+
classifyMemoryType(content) {
|
|
165
|
+
const lowerContent = content.toLowerCase();
|
|
166
|
+
// Check for preference indicators
|
|
167
|
+
const preferencePatterns = [
|
|
168
|
+
/i (like|love|prefer|enjoy|hate|dislike|don't like)/i,
|
|
169
|
+
/my favorite/i,
|
|
170
|
+
/i('m| am) (a fan of|into|interested in)/i,
|
|
171
|
+
];
|
|
172
|
+
if (preferencePatterns.some(p => p.test(lowerContent))) {
|
|
173
|
+
return 'preference';
|
|
174
|
+
}
|
|
175
|
+
// Check for factual information about the user
|
|
176
|
+
const factPatterns = [
|
|
177
|
+
/i (am|'m) (a |an )?(\w+ )?(developer|engineer|designer|student|teacher|doctor|lawyer)/i,
|
|
178
|
+
/i (work|live|study) (at|in|for)/i,
|
|
179
|
+
/my (name|job|profession|age|location|birthday)/i,
|
|
180
|
+
/i have (a |an )?(\d+ )?(kids?|children|dogs?|cats?|pets?)/i,
|
|
181
|
+
];
|
|
182
|
+
if (factPatterns.some(p => p.test(lowerContent))) {
|
|
183
|
+
return 'fact';
|
|
184
|
+
}
|
|
185
|
+
// Check for emotional content
|
|
186
|
+
const emotionalPatterns = [
|
|
187
|
+
/i('m| am) (feeling|so|really|very) (happy|sad|excited|anxious|worried|stressed|grateful)/i,
|
|
188
|
+
/thank you|thanks|appreciate/i,
|
|
189
|
+
/i('m| am) (sorry|apologize)/i,
|
|
190
|
+
/(love|hate) (this|that|it)/i,
|
|
191
|
+
];
|
|
192
|
+
if (emotionalPatterns.some(p => p.test(lowerContent))) {
|
|
193
|
+
return 'emotional';
|
|
194
|
+
}
|
|
195
|
+
// Check for instructions
|
|
196
|
+
const instructionPatterns = [
|
|
197
|
+
/please (always|never|remember|don't|do not)/i,
|
|
198
|
+
/i want you to/i,
|
|
199
|
+
/can you (please )?make sure/i,
|
|
200
|
+
/when (i ask|responding|you)/i,
|
|
201
|
+
];
|
|
202
|
+
if (instructionPatterns.some(p => p.test(lowerContent))) {
|
|
203
|
+
return 'instruction';
|
|
204
|
+
}
|
|
205
|
+
// Check for experiences/stories
|
|
206
|
+
const experiencePatterns = [
|
|
207
|
+
/i (went|did|saw|visited|attended|met|had)/i,
|
|
208
|
+
/yesterday|last (week|month|year)|recently/i,
|
|
209
|
+
/one time|once upon a time|i remember when/i,
|
|
210
|
+
];
|
|
211
|
+
if (experiencePatterns.some(p => p.test(lowerContent))) {
|
|
212
|
+
return 'experience';
|
|
213
|
+
}
|
|
214
|
+
return 'general';
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Calculate enhanced importance score based on multiple factors
|
|
218
|
+
*/
|
|
219
|
+
calculateEnhancedImportance(content, memoryType, _context) {
|
|
220
|
+
let score = 0.5; // Base score
|
|
221
|
+
// Adjust based on memory type
|
|
222
|
+
const typeWeights = {
|
|
223
|
+
instruction: 0.9, // Instructions are very important
|
|
224
|
+
fact: 0.8, // Facts about user are important
|
|
225
|
+
preference: 0.75, // Preferences matter
|
|
226
|
+
emotional: 0.7, // Emotional moments are memorable
|
|
227
|
+
experience: 0.6, // Experiences are contextual
|
|
228
|
+
context: 0.4, // Context is temporary
|
|
229
|
+
general: 0.5, // Default
|
|
230
|
+
};
|
|
231
|
+
score = typeWeights[memoryType];
|
|
232
|
+
// Adjust based on content length (longer = potentially more detailed/important)
|
|
233
|
+
const wordCount = content.split(/\s+/).length;
|
|
234
|
+
if (wordCount > 50)
|
|
235
|
+
score = Math.min(1.0, score + 0.1);
|
|
236
|
+
else if (wordCount < 10)
|
|
237
|
+
score = Math.max(0.1, score - 0.1);
|
|
238
|
+
// Adjust based on specificity (names, numbers, dates)
|
|
239
|
+
const specificityIndicators = [
|
|
240
|
+
/\b\d{4}\b/, // Years
|
|
241
|
+
/\b\d{1,2}\/\d{1,2}\b/, // Dates
|
|
242
|
+
/\b[A-Z][a-z]+\b/, // Proper nouns
|
|
243
|
+
/\b\d+\s*(years?|months?|days?|hours?)\b/i, // Time durations
|
|
244
|
+
];
|
|
245
|
+
const specificityCount = specificityIndicators.filter(p => p.test(content)).length;
|
|
246
|
+
score = Math.min(1.0, score + specificityCount * 0.05);
|
|
247
|
+
// Adjust based on question presence (questions often indicate important topics)
|
|
248
|
+
if (content.includes('?')) {
|
|
249
|
+
score = Math.min(1.0, score + 0.05);
|
|
250
|
+
}
|
|
251
|
+
// Clamp between 0.1 and 1.0
|
|
252
|
+
return Math.max(0.1, Math.min(1.0, score));
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Apply time-based decay to importance score
|
|
256
|
+
*/
|
|
257
|
+
applyDecay(originalImportance, timestamp, accessCount = 0, lastAccessed) {
|
|
258
|
+
const now = Date.now();
|
|
259
|
+
const ageInDays = (now - timestamp) / (1000 * 60 * 60 * 24);
|
|
260
|
+
const timeSinceAccess = lastAccessed
|
|
261
|
+
? (now - lastAccessed) / (1000 * 60 * 60 * 24)
|
|
262
|
+
: ageInDays;
|
|
263
|
+
// Base decay: memories lose ~10% importance per month if not accessed
|
|
264
|
+
const decayRate = 0.003; // ~10% per month
|
|
265
|
+
let decayedImportance = originalImportance * Math.exp(-decayRate * timeSinceAccess);
|
|
266
|
+
// Boost for frequently accessed memories (reinforcement)
|
|
267
|
+
const accessBoost = Math.min(0.3, accessCount * 0.02);
|
|
268
|
+
decayedImportance = Math.min(1.0, decayedImportance + accessBoost);
|
|
269
|
+
// Never let importance drop below 0.1 for recent memories (< 7 days)
|
|
270
|
+
if (ageInDays < 7) {
|
|
271
|
+
decayedImportance = Math.max(0.3, decayedImportance);
|
|
272
|
+
}
|
|
273
|
+
return Math.max(0.1, Math.min(1.0, decayedImportance));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Get available embedding models
|
|
277
|
+
*/
|
|
278
|
+
getEmbeddingModels() {
|
|
279
|
+
return this.embeddingModels;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Generate embedding for text using specified model
|
|
283
|
+
*/
|
|
284
|
+
async generateEmbedding(text, model) {
|
|
285
|
+
try {
|
|
286
|
+
// For now, use Ollama for embedding generation
|
|
287
|
+
// TODO: Add support for other providers (OpenAI, Sentence Transformers, etc.)
|
|
288
|
+
const response = await ollamaService.generateEmbeddings({
|
|
289
|
+
model,
|
|
290
|
+
input: text,
|
|
291
|
+
});
|
|
292
|
+
return response.embeddings[0] || null;
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
console.error('Failed to generate embedding:', error);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Store a memory entry with embedding and automatic classification
|
|
301
|
+
*/
|
|
302
|
+
async storeMemory(userId, personaId, content, embeddingModel, context, importanceScore, memoryType) {
|
|
303
|
+
const id = uuidv4();
|
|
304
|
+
const timestamp = Date.now();
|
|
305
|
+
// Auto-classify memory type if not provided
|
|
306
|
+
const classifiedType = memoryType || this.classifyMemoryType(content);
|
|
307
|
+
// Calculate enhanced importance if not provided
|
|
308
|
+
const calculatedImportance = importanceScore !== undefined
|
|
309
|
+
? importanceScore
|
|
310
|
+
: this.calculateEnhancedImportance(content, classifiedType, context);
|
|
311
|
+
// Check for similar existing memories before storing
|
|
312
|
+
const existingSimilar = await this.findSimilarMemories(userId, personaId, content, embeddingModel, 0.85 // High similarity threshold for deduplication
|
|
313
|
+
);
|
|
314
|
+
// If very similar memory exists, reinforce it instead of creating new
|
|
315
|
+
if (existingSimilar.length > 0) {
|
|
316
|
+
const mostSimilar = existingSimilar[0];
|
|
317
|
+
console.log(`[MEMORY] Found similar memory (${(mostSimilar.similarity_score * 100).toFixed(1)}% similar), reinforcing instead of creating new`);
|
|
318
|
+
await this.reinforceMemory(mostSimilar.entry.id);
|
|
319
|
+
return mostSimilar.entry;
|
|
320
|
+
}
|
|
321
|
+
// Generate embedding
|
|
322
|
+
const embedding = await this.generateEmbedding(content, embeddingModel);
|
|
323
|
+
const db = this.ensureDatabase();
|
|
324
|
+
const stmt = db.prepare(`
|
|
325
|
+
INSERT INTO persona_memories (
|
|
326
|
+
id, user_id, persona_id, content, embedding, timestamp, context, importance_score,
|
|
327
|
+
memory_type, access_count, last_accessed, decay_factor
|
|
328
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
329
|
+
`);
|
|
330
|
+
stmt.run(id, userId, personaId, content, embedding ? Buffer.from(new Float32Array(embedding).buffer) : null, timestamp, context || null, calculatedImportance, classifiedType, 0, // access_count
|
|
331
|
+
null, // last_accessed
|
|
332
|
+
1.0 // decay_factor
|
|
333
|
+
);
|
|
334
|
+
console.log(`[MEMORY] Stored: type=${classifiedType}, importance=${calculatedImportance.toFixed(2)}, id=${id}, content="${content.substring(0, 50)}..."`);
|
|
335
|
+
return {
|
|
336
|
+
id,
|
|
337
|
+
user_id: userId,
|
|
338
|
+
persona_id: personaId,
|
|
339
|
+
content,
|
|
340
|
+
embedding: embedding || undefined,
|
|
341
|
+
timestamp,
|
|
342
|
+
context,
|
|
343
|
+
importance_score: calculatedImportance,
|
|
344
|
+
memory_type: classifiedType,
|
|
345
|
+
access_count: 0,
|
|
346
|
+
decay_factor: 1.0,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Find similar memories (for deduplication)
|
|
351
|
+
*/
|
|
352
|
+
async findSimilarMemories(userId, personaId, content, embeddingModel, minSimilarity) {
|
|
353
|
+
const queryEmbedding = await this.generateEmbedding(content, embeddingModel);
|
|
354
|
+
if (!queryEmbedding)
|
|
355
|
+
return [];
|
|
356
|
+
const db = this.ensureDatabase();
|
|
357
|
+
const stmt = db.prepare(`
|
|
358
|
+
SELECT id, user_id, persona_id, content, embedding, timestamp, context, importance_score,
|
|
359
|
+
memory_type, access_count, last_accessed, decay_factor
|
|
360
|
+
FROM persona_memories
|
|
361
|
+
WHERE user_id = ? AND persona_id = ? AND embedding IS NOT NULL
|
|
362
|
+
ORDER BY timestamp DESC
|
|
363
|
+
LIMIT 50
|
|
364
|
+
`);
|
|
365
|
+
const memories = stmt.all(userId, personaId);
|
|
366
|
+
const results = [];
|
|
367
|
+
for (const memory of memories) {
|
|
368
|
+
const embeddingArray = Array.from(new Float32Array(memory.embedding.buffer));
|
|
369
|
+
const similarity = this.cosineSimilarity(queryEmbedding, embeddingArray);
|
|
370
|
+
if (similarity >= minSimilarity) {
|
|
371
|
+
results.push({
|
|
372
|
+
entry: {
|
|
373
|
+
id: memory.id,
|
|
374
|
+
user_id: memory.user_id,
|
|
375
|
+
persona_id: memory.persona_id,
|
|
376
|
+
content: memory.content,
|
|
377
|
+
embedding: embeddingArray,
|
|
378
|
+
timestamp: memory.timestamp,
|
|
379
|
+
context: memory.context || undefined,
|
|
380
|
+
importance_score: memory.importance_score,
|
|
381
|
+
},
|
|
382
|
+
similarity_score: similarity,
|
|
383
|
+
relevance_rank: 0,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return results.sort((a, b) => b.similarity_score - a.similarity_score);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Reinforce a memory (increases importance and access count)
|
|
391
|
+
*/
|
|
392
|
+
async reinforceMemory(memoryId) {
|
|
393
|
+
const db = this.ensureDatabase();
|
|
394
|
+
const stmt = db.prepare(`
|
|
395
|
+
UPDATE persona_memories
|
|
396
|
+
SET access_count = COALESCE(access_count, 0) + 1,
|
|
397
|
+
last_accessed = ?,
|
|
398
|
+
importance_score = MIN(1.0, COALESCE(importance_score, 0.5) + 0.05)
|
|
399
|
+
WHERE id = ?
|
|
400
|
+
`);
|
|
401
|
+
const result = stmt.run(Date.now(), memoryId);
|
|
402
|
+
return result.changes > 0;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Calculate cosine similarity between two vectors
|
|
406
|
+
*/
|
|
407
|
+
cosineSimilarity(a, b) {
|
|
408
|
+
if (a.length !== b.length)
|
|
409
|
+
return 0;
|
|
410
|
+
let dotProduct = 0;
|
|
411
|
+
let normA = 0;
|
|
412
|
+
let normB = 0;
|
|
413
|
+
for (let i = 0; i < a.length; i++) {
|
|
414
|
+
dotProduct += a[i] * b[i];
|
|
415
|
+
normA += a[i] * a[i];
|
|
416
|
+
normB += b[i] * b[i];
|
|
417
|
+
}
|
|
418
|
+
if (normA === 0 || normB === 0)
|
|
419
|
+
return 0;
|
|
420
|
+
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Search memories using semantic similarity with enhanced relevance scoring
|
|
424
|
+
*/
|
|
425
|
+
async searchMemories(userId, personaId, query, embeddingModel, topK = 5, minSimilarity = 0.3, memoryTypes // Optional filter by memory types
|
|
426
|
+
) {
|
|
427
|
+
// Generate embedding for query
|
|
428
|
+
const queryEmbedding = await this.generateEmbedding(query, embeddingModel);
|
|
429
|
+
if (!queryEmbedding) {
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
// Get all memories for this user/persona with embeddings
|
|
433
|
+
const db = this.ensureDatabase();
|
|
434
|
+
let sql = `
|
|
435
|
+
SELECT id, user_id, persona_id, content, embedding, timestamp, context, importance_score,
|
|
436
|
+
memory_type, access_count, last_accessed, decay_factor
|
|
437
|
+
FROM persona_memories
|
|
438
|
+
WHERE user_id = ? AND persona_id = ? AND embedding IS NOT NULL
|
|
439
|
+
`;
|
|
440
|
+
// Add memory type filter if specified
|
|
441
|
+
if (memoryTypes && memoryTypes.length > 0) {
|
|
442
|
+
sql += ` AND memory_type IN (${memoryTypes.map(() => '?').join(',')})`;
|
|
443
|
+
}
|
|
444
|
+
sql += ` ORDER BY timestamp DESC`;
|
|
445
|
+
const stmt = db.prepare(sql);
|
|
446
|
+
const params = memoryTypes
|
|
447
|
+
? [userId, personaId, ...memoryTypes]
|
|
448
|
+
: [userId, personaId];
|
|
449
|
+
const memories = stmt.all(...params);
|
|
450
|
+
// Calculate enhanced relevance scores
|
|
451
|
+
const results = [];
|
|
452
|
+
for (const memory of memories) {
|
|
453
|
+
// Convert buffer back to float array
|
|
454
|
+
const embeddingArray = Array.from(new Float32Array(memory.embedding.buffer));
|
|
455
|
+
const similarity = this.cosineSimilarity(queryEmbedding, embeddingArray);
|
|
456
|
+
if (similarity >= minSimilarity) {
|
|
457
|
+
// Calculate decayed importance
|
|
458
|
+
const decayedImportance = this.applyDecay(memory.importance_score, memory.timestamp, memory.access_count || 0, memory.last_accessed || undefined);
|
|
459
|
+
results.push({
|
|
460
|
+
entry: {
|
|
461
|
+
id: memory.id,
|
|
462
|
+
user_id: memory.user_id,
|
|
463
|
+
persona_id: memory.persona_id,
|
|
464
|
+
content: memory.content,
|
|
465
|
+
embedding: embeddingArray,
|
|
466
|
+
timestamp: memory.timestamp,
|
|
467
|
+
context: memory.context || undefined,
|
|
468
|
+
importance_score: decayedImportance,
|
|
469
|
+
},
|
|
470
|
+
similarity_score: similarity,
|
|
471
|
+
relevance_rank: 0, // Will be set after sorting
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Sort by composite relevance score
|
|
476
|
+
results.sort((a, b) => {
|
|
477
|
+
const now = Date.now();
|
|
478
|
+
const ageA = (now - a.entry.timestamp) / (1000 * 60 * 60);
|
|
479
|
+
const ageB = (now - b.entry.timestamp) / (1000 * 60 * 60);
|
|
480
|
+
const recencyA = ageA < 24 ? 0.1 : ageA < 168 ? 0.05 : 0;
|
|
481
|
+
const recencyB = ageB < 24 ? 0.1 : ageB < 168 ? 0.05 : 0;
|
|
482
|
+
const scoreA = a.similarity_score * 0.5 +
|
|
483
|
+
(a.entry.importance_score || 0.5) * 0.25 +
|
|
484
|
+
recencyA;
|
|
485
|
+
const scoreB = b.similarity_score * 0.5 +
|
|
486
|
+
(b.entry.importance_score || 0.5) * 0.25 +
|
|
487
|
+
recencyB;
|
|
488
|
+
return scoreB - scoreA;
|
|
489
|
+
});
|
|
490
|
+
// Update access count for returned memories
|
|
491
|
+
const topResults = results.slice(0, topK);
|
|
492
|
+
for (const result of topResults) {
|
|
493
|
+
await this.updateMemoryAccess(result.entry.id);
|
|
494
|
+
}
|
|
495
|
+
// Set relevance ranks and return top-k
|
|
496
|
+
return topResults.map((result, index) => ({
|
|
497
|
+
...result,
|
|
498
|
+
relevance_rank: index + 1,
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Update memory access tracking
|
|
503
|
+
*/
|
|
504
|
+
async updateMemoryAccess(memoryId) {
|
|
505
|
+
try {
|
|
506
|
+
const db = this.ensureDatabase();
|
|
507
|
+
const stmt = db.prepare(`
|
|
508
|
+
UPDATE persona_memories
|
|
509
|
+
SET access_count = COALESCE(access_count, 0) + 1,
|
|
510
|
+
last_accessed = ?
|
|
511
|
+
WHERE id = ?
|
|
512
|
+
`);
|
|
513
|
+
stmt.run(Date.now(), memoryId);
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
console.warn('[MemoryService] Failed to update memory access:', error);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Get all memories for a persona
|
|
521
|
+
*/
|
|
522
|
+
async getMemories(userId, personaId, limit = 100, offset = 0) {
|
|
523
|
+
const db = this.ensureDatabase();
|
|
524
|
+
const stmt = db.prepare(`
|
|
525
|
+
SELECT id, user_id, persona_id, content, embedding, timestamp, context, importance_score
|
|
526
|
+
FROM persona_memories
|
|
527
|
+
WHERE user_id = ? AND persona_id = ?
|
|
528
|
+
ORDER BY timestamp DESC
|
|
529
|
+
LIMIT ? OFFSET ?
|
|
530
|
+
`);
|
|
531
|
+
const memories = stmt.all(userId, personaId, limit, offset);
|
|
532
|
+
return memories.map(memory => ({
|
|
533
|
+
id: memory.id,
|
|
534
|
+
user_id: memory.user_id,
|
|
535
|
+
persona_id: memory.persona_id,
|
|
536
|
+
content: memory.content,
|
|
537
|
+
embedding: memory.embedding
|
|
538
|
+
? Array.from(new Float32Array(memory.embedding.buffer))
|
|
539
|
+
: undefined,
|
|
540
|
+
timestamp: memory.timestamp,
|
|
541
|
+
context: memory.context || undefined,
|
|
542
|
+
importance_score: memory.importance_score,
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Get memory count for a persona
|
|
547
|
+
*/
|
|
548
|
+
async getMemoryCount(userId, personaId) {
|
|
549
|
+
console.log(`[MEMORY-DEBUG] getMemoryCount called - userId: ${userId}, personaId: ${personaId}`);
|
|
550
|
+
const db = this.ensureDatabase();
|
|
551
|
+
const stmt = db.prepare(`
|
|
552
|
+
SELECT COUNT(*) as count
|
|
553
|
+
FROM persona_memories
|
|
554
|
+
WHERE user_id = ? AND persona_id = ?
|
|
555
|
+
`);
|
|
556
|
+
const result = stmt.get(userId, personaId);
|
|
557
|
+
console.log(`[MEMORY-DEBUG] Query result:`, result);
|
|
558
|
+
console.log(`[MEMORY-DEBUG] Memory count: ${result.count}`);
|
|
559
|
+
return result.count;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Get memory status for a persona
|
|
563
|
+
*/
|
|
564
|
+
async getMemoryStatus(userId, personaId) {
|
|
565
|
+
try {
|
|
566
|
+
const memoryCount = await this.getMemoryCount(userId, personaId);
|
|
567
|
+
// Calculate approximate memory size (rough estimate)
|
|
568
|
+
const avgMemorySize = 1024; // Average bytes per memory entry
|
|
569
|
+
const sizeMb = (memoryCount * avgMemorySize) / (1024 * 1024);
|
|
570
|
+
return {
|
|
571
|
+
memory_count: memoryCount,
|
|
572
|
+
last_backup: undefined, // Could be implemented later
|
|
573
|
+
size_mb: Math.round(sizeMb * 100) / 100, // Round to 2 decimal places
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
console.error('Error getting memory status:', error);
|
|
578
|
+
throw new Error('Failed to get memory status');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Delete all memories for a persona (wipe)
|
|
583
|
+
*/
|
|
584
|
+
async wipeMemories(userId, personaId) {
|
|
585
|
+
const db = this.ensureDatabase();
|
|
586
|
+
const stmt = db.prepare(`
|
|
587
|
+
DELETE FROM persona_memories
|
|
588
|
+
WHERE user_id = ? AND persona_id = ?
|
|
589
|
+
`);
|
|
590
|
+
const result = stmt.run(userId, personaId);
|
|
591
|
+
return result.changes;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Export memories for backup
|
|
595
|
+
*/
|
|
596
|
+
async exportMemories(userId, personaId) {
|
|
597
|
+
return this.getMemories(userId, personaId, 10000); // Export all
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Import memories from backup
|
|
601
|
+
*/
|
|
602
|
+
async importMemories(memories, targetUserId) {
|
|
603
|
+
let imported = 0;
|
|
604
|
+
const db = this.ensureDatabase();
|
|
605
|
+
const stmt = db.prepare(`
|
|
606
|
+
INSERT INTO persona_memories (
|
|
607
|
+
id, user_id, persona_id, content, embedding, timestamp, context, importance_score
|
|
608
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
609
|
+
`);
|
|
610
|
+
for (const memory of memories) {
|
|
611
|
+
try {
|
|
612
|
+
stmt.run(memory.id, targetUserId, // Use target user ID
|
|
613
|
+
memory.persona_id, memory.content, memory.embedding
|
|
614
|
+
? Buffer.from(new Float32Array(memory.embedding).buffer)
|
|
615
|
+
: null, memory.timestamp, memory.context || null, memory.importance_score || 0.5);
|
|
616
|
+
imported++;
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
console.error('Failed to import memory:', error);
|
|
620
|
+
// Continue with next memory
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return imported;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Update memory importance score
|
|
627
|
+
*/
|
|
628
|
+
async updateMemoryImportance(memoryId, importanceScore) {
|
|
629
|
+
const db = this.ensureDatabase();
|
|
630
|
+
const stmt = db.prepare(`
|
|
631
|
+
UPDATE persona_memories
|
|
632
|
+
SET importance_score = ?
|
|
633
|
+
WHERE id = ?
|
|
634
|
+
`);
|
|
635
|
+
const result = stmt.run(importanceScore, memoryId);
|
|
636
|
+
return result.changes > 0;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Delete old memories based on retention policy
|
|
640
|
+
*/
|
|
641
|
+
async cleanupOldMemories(userId, personaId, retentionDays) {
|
|
642
|
+
const cutoffTimestamp = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
643
|
+
const db = this.ensureDatabase();
|
|
644
|
+
const stmt = db.prepare(`
|
|
645
|
+
DELETE FROM persona_memories
|
|
646
|
+
WHERE user_id = ? AND persona_id = ? AND timestamp < ?
|
|
647
|
+
AND importance_score < 0.7
|
|
648
|
+
`);
|
|
649
|
+
const result = stmt.run(userId, personaId, cutoffTimestamp);
|
|
650
|
+
return result.changes;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Consolidate similar memories to reduce redundancy and save space
|
|
654
|
+
* This merges memories with high similarity into a single, more comprehensive memory
|
|
655
|
+
*/
|
|
656
|
+
async consolidateMemories(userId, personaId, embeddingModel, similarityThreshold = 0.8) {
|
|
657
|
+
const db = this.ensureDatabase();
|
|
658
|
+
let consolidated = 0;
|
|
659
|
+
let deleted = 0;
|
|
660
|
+
try {
|
|
661
|
+
// Get all memories with embeddings
|
|
662
|
+
const stmt = db.prepare(`
|
|
663
|
+
SELECT id, content, embedding, timestamp, importance_score, memory_type, access_count
|
|
664
|
+
FROM persona_memories
|
|
665
|
+
WHERE user_id = ? AND persona_id = ? AND embedding IS NOT NULL
|
|
666
|
+
ORDER BY importance_score DESC, timestamp DESC
|
|
667
|
+
`);
|
|
668
|
+
const memories = stmt.all(userId, personaId);
|
|
669
|
+
const processedIds = new Set();
|
|
670
|
+
const toDelete = [];
|
|
671
|
+
for (let i = 0; i < memories.length; i++) {
|
|
672
|
+
const memory = memories[i];
|
|
673
|
+
if (processedIds.has(memory.id))
|
|
674
|
+
continue;
|
|
675
|
+
const embeddingA = Array.from(new Float32Array(memory.embedding.buffer));
|
|
676
|
+
const similarMemories = [];
|
|
677
|
+
// Find similar memories
|
|
678
|
+
for (let j = i + 1; j < memories.length; j++) {
|
|
679
|
+
const other = memories[j];
|
|
680
|
+
if (processedIds.has(other.id))
|
|
681
|
+
continue;
|
|
682
|
+
const embeddingB = Array.from(new Float32Array(other.embedding.buffer));
|
|
683
|
+
const similarity = this.cosineSimilarity(embeddingA, embeddingB);
|
|
684
|
+
if (similarity >= similarityThreshold) {
|
|
685
|
+
similarMemories.push(other);
|
|
686
|
+
processedIds.add(other.id);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
// If we found similar memories, consolidate them
|
|
690
|
+
if (similarMemories.length > 0) {
|
|
691
|
+
processedIds.add(memory.id);
|
|
692
|
+
// Create consolidated content
|
|
693
|
+
const allContent = [
|
|
694
|
+
memory.content,
|
|
695
|
+
...similarMemories.map(m => m.content),
|
|
696
|
+
];
|
|
697
|
+
const consolidatedContent = this.createConsolidatedContent(allContent);
|
|
698
|
+
// Calculate combined importance (weighted average with boost)
|
|
699
|
+
const allImportances = [
|
|
700
|
+
memory.importance_score,
|
|
701
|
+
...similarMemories.map(m => m.importance_score),
|
|
702
|
+
];
|
|
703
|
+
const avgImportance = allImportances.reduce((a, b) => a + b, 0) / allImportances.length;
|
|
704
|
+
const consolidatedImportance = Math.min(1.0, avgImportance * 1.1); // 10% boost for consolidation
|
|
705
|
+
// Use the most common memory type
|
|
706
|
+
const types = [
|
|
707
|
+
memory.memory_type,
|
|
708
|
+
...similarMemories.map(m => m.memory_type),
|
|
709
|
+
].filter(Boolean);
|
|
710
|
+
const typeCount = {};
|
|
711
|
+
types.forEach(t => {
|
|
712
|
+
if (t)
|
|
713
|
+
typeCount[t] = (typeCount[t] || 0) + 1;
|
|
714
|
+
});
|
|
715
|
+
const consolidatedType = Object.entries(typeCount).sort((a, b) => b[1] - a[1])[0]?.[0] ||
|
|
716
|
+
'general';
|
|
717
|
+
// Generate new embedding for consolidated content
|
|
718
|
+
const newEmbedding = await this.generateEmbedding(consolidatedContent, embeddingModel);
|
|
719
|
+
// Store consolidated memory
|
|
720
|
+
const consolidatedFromIds = [
|
|
721
|
+
memory.id,
|
|
722
|
+
...similarMemories.map(m => m.id),
|
|
723
|
+
];
|
|
724
|
+
const insertStmt = db.prepare(`
|
|
725
|
+
INSERT INTO persona_memories (
|
|
726
|
+
id, user_id, persona_id, content, embedding, timestamp, context, importance_score,
|
|
727
|
+
memory_type, access_count, last_accessed, decay_factor, consolidated_from
|
|
728
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
729
|
+
`);
|
|
730
|
+
insertStmt.run(uuidv4(), userId, personaId, consolidatedContent, newEmbedding
|
|
731
|
+
? Buffer.from(new Float32Array(newEmbedding).buffer)
|
|
732
|
+
: null, Date.now(), `Consolidated from ${consolidatedFromIds.length} memories`, consolidatedImportance, consolidatedType, similarMemories.reduce((sum, m) => sum + (m.access_count || 0), memory.access_count || 0), Date.now(), 1.0, JSON.stringify(consolidatedFromIds));
|
|
733
|
+
// Mark original memories for deletion
|
|
734
|
+
toDelete.push(...consolidatedFromIds);
|
|
735
|
+
consolidated++;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Delete original memories that were consolidated
|
|
739
|
+
if (toDelete.length > 0) {
|
|
740
|
+
const deleteStmt = db.prepare(`
|
|
741
|
+
DELETE FROM persona_memories WHERE id IN (${toDelete.map(() => '?').join(',')})
|
|
742
|
+
`);
|
|
743
|
+
deleteStmt.run(...toDelete);
|
|
744
|
+
deleted = toDelete.length;
|
|
745
|
+
}
|
|
746
|
+
console.log(`[MEMORY] Consolidation complete: ${consolidated} groups merged, ${deleted} memories deleted`);
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
console.error('[MEMORY] Consolidation error:', error);
|
|
750
|
+
}
|
|
751
|
+
return { consolidated, deleted };
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Create consolidated content from multiple similar memories
|
|
755
|
+
*/
|
|
756
|
+
createConsolidatedContent(contents) {
|
|
757
|
+
if (contents.length === 1)
|
|
758
|
+
return contents[0];
|
|
759
|
+
// For now, use the longest content as base and note the count
|
|
760
|
+
const sorted = [...contents].sort((a, b) => b.length - a.length);
|
|
761
|
+
const base = sorted[0];
|
|
762
|
+
// Add a note about consolidation
|
|
763
|
+
if (contents.length === 2) {
|
|
764
|
+
return base;
|
|
765
|
+
}
|
|
766
|
+
return `${base} (consolidated from ${contents.length} related interactions)`;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Get memory statistics for a persona
|
|
770
|
+
*/
|
|
771
|
+
async getMemoryStats(userId, personaId) {
|
|
772
|
+
const db = this.ensureDatabase();
|
|
773
|
+
// Get counts by type
|
|
774
|
+
const typeStmt = db.prepare(`
|
|
775
|
+
SELECT memory_type, COUNT(*) as count
|
|
776
|
+
FROM persona_memories
|
|
777
|
+
WHERE user_id = ? AND persona_id = ?
|
|
778
|
+
GROUP BY memory_type
|
|
779
|
+
`);
|
|
780
|
+
const typeCounts = typeStmt.all(userId, personaId);
|
|
781
|
+
// Get aggregate stats
|
|
782
|
+
const statsStmt = db.prepare(`
|
|
783
|
+
SELECT
|
|
784
|
+
COUNT(*) as total_count,
|
|
785
|
+
AVG(importance_score) as avg_importance,
|
|
786
|
+
MIN(timestamp) as oldest_memory,
|
|
787
|
+
MAX(timestamp) as newest_memory,
|
|
788
|
+
SUM(COALESCE(access_count, 0)) as total_accesses
|
|
789
|
+
FROM persona_memories
|
|
790
|
+
WHERE user_id = ? AND persona_id = ?
|
|
791
|
+
`);
|
|
792
|
+
const stats = statsStmt.get(userId, personaId);
|
|
793
|
+
const byType = {};
|
|
794
|
+
typeCounts.forEach(({ memory_type, count }) => {
|
|
795
|
+
byType[memory_type || 'general'] = count;
|
|
796
|
+
});
|
|
797
|
+
return {
|
|
798
|
+
total_count: stats.total_count,
|
|
799
|
+
by_type: byType,
|
|
800
|
+
avg_importance: stats.avg_importance || 0.5,
|
|
801
|
+
oldest_memory: stats.oldest_memory,
|
|
802
|
+
newest_memory: stats.newest_memory,
|
|
803
|
+
total_accesses: stats.total_accesses || 0,
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Apply decay to all memories (should be called periodically)
|
|
808
|
+
*/
|
|
809
|
+
async applyGlobalDecay(userId, personaId) {
|
|
810
|
+
const db = this.ensureDatabase();
|
|
811
|
+
// Get all memories
|
|
812
|
+
const selectStmt = db.prepare(`
|
|
813
|
+
SELECT id, importance_score, timestamp, access_count, last_accessed
|
|
814
|
+
FROM persona_memories
|
|
815
|
+
WHERE user_id = ? AND persona_id = ?
|
|
816
|
+
`);
|
|
817
|
+
const memories = selectStmt.all(userId, personaId);
|
|
818
|
+
let updated = 0;
|
|
819
|
+
const updateStmt = db.prepare(`
|
|
820
|
+
UPDATE persona_memories
|
|
821
|
+
SET importance_score = ?, decay_factor = ?
|
|
822
|
+
WHERE id = ?
|
|
823
|
+
`);
|
|
824
|
+
for (const memory of memories) {
|
|
825
|
+
const newImportance = this.applyDecay(memory.importance_score, memory.timestamp, memory.access_count || 0, memory.last_accessed || undefined);
|
|
826
|
+
// Only update if importance changed significantly
|
|
827
|
+
if (Math.abs(newImportance - memory.importance_score) > 0.01) {
|
|
828
|
+
const decayFactor = newImportance / memory.importance_score;
|
|
829
|
+
updateStmt.run(newImportance, decayFactor, memory.id);
|
|
830
|
+
updated++;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
console.log(`[MEMORY] Applied decay to ${updated} memories`);
|
|
834
|
+
return updated;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Get important memories (facts, preferences, instructions) that should always be included
|
|
838
|
+
*/
|
|
839
|
+
async getCoreMemories(userId, personaId, limit = 5) {
|
|
840
|
+
const db = this.ensureDatabase();
|
|
841
|
+
const stmt = db.prepare(`
|
|
842
|
+
SELECT id, user_id, persona_id, content, embedding, timestamp, context, importance_score,
|
|
843
|
+
memory_type, access_count, last_accessed
|
|
844
|
+
FROM persona_memories
|
|
845
|
+
WHERE user_id = ? AND persona_id = ?
|
|
846
|
+
AND memory_type IN ('fact', 'preference', 'instruction')
|
|
847
|
+
AND importance_score >= 0.7
|
|
848
|
+
ORDER BY importance_score DESC, access_count DESC
|
|
849
|
+
LIMIT ?
|
|
850
|
+
`);
|
|
851
|
+
const memories = stmt.all(userId, personaId, limit);
|
|
852
|
+
return memories.map(memory => ({
|
|
853
|
+
id: memory.id,
|
|
854
|
+
user_id: memory.user_id,
|
|
855
|
+
persona_id: memory.persona_id,
|
|
856
|
+
content: memory.content,
|
|
857
|
+
embedding: memory.embedding
|
|
858
|
+
? Array.from(new Float32Array(memory.embedding.buffer))
|
|
859
|
+
: undefined,
|
|
860
|
+
timestamp: memory.timestamp,
|
|
861
|
+
context: memory.context || undefined,
|
|
862
|
+
importance_score: memory.importance_score,
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
export const memoryService = new MemoryService();
|
|
867
|
+
//# sourceMappingURL=memoryService.js.map
|