llmjs2 1.3.9 → 1.6.1
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/README.md +31 -476
- package/chain/AGENT_STEP_README.md +102 -0
- package/chain/README.md +257 -0
- package/chain/WORKFLOW_README.md +85 -0
- package/chain/agent-step-example.js +232 -0
- package/chain/docs/AGENT.md +126 -0
- package/chain/docs/GRAPH.md +490 -0
- package/chain/examples.js +314 -0
- package/chain/index.js +31 -0
- package/chain/lib/agent.js +338 -0
- package/chain/lib/flow/agent-step.js +119 -0
- package/chain/lib/flow/edge.js +24 -0
- package/chain/lib/flow/flow.js +76 -0
- package/chain/lib/flow/graph.js +331 -0
- package/chain/lib/flow/index.js +7 -0
- package/chain/lib/flow/step.js +63 -0
- package/chain/lib/memory/in-memory.js +117 -0
- package/chain/lib/memory/index.js +36 -0
- package/chain/lib/memory/lance-memory.js +225 -0
- package/chain/lib/memory/sqlite-memory.js +309 -0
- package/chain/simple-agent-step-example.js +168 -0
- package/chain/workflow-example-usage.js +70 -0
- package/chain/workflow-example.json +59 -0
- package/core/README.md +485 -0
- package/core/cli.js +275 -0
- package/core/docs/BASIC_USAGE.md +62 -0
- package/core/docs/CLI.md +104 -0
- package/{docs → core/docs}/GET_STARTED.md +129 -129
- package/{docs → core/docs}/GUARDRAILS_GUIDE.md +734 -734
- package/{docs → core/docs}/README.md +47 -47
- package/core/docs/ROUTER_GUIDE.md +199 -0
- package/{docs → core/docs}/SERVER_MODE.md +358 -350
- package/core/index.js +115 -0
- package/{providers → core/providers}/ollama.js +14 -6
- package/{providers → core/providers}/openai.js +14 -6
- package/{providers → core/providers}/openrouter.js +14 -6
- package/core/router.js +252 -0
- package/{server.js → core/server.js} +15 -5
- package/package.json +43 -27
- package/cli.js +0 -195
- package/docs/BASIC_USAGE.md +0 -296
- package/docs/CLI.md +0 -455
- package/docs/ROUTER_GUIDE.md +0 -402
- package/index.js +0 -267
- package/router.js +0 -273
- package/test-completion.js +0 -99
- package/test.js +0 -246
- /package/{config.yaml → core/config.yaml} +0 -0
- /package/{logger.js → core/logger.js} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InMemory class for managing conversation history and data storage
|
|
3
|
+
* Provides methods to save, retrieve, and search conversation data
|
|
4
|
+
*/
|
|
5
|
+
class InMemory {
|
|
6
|
+
/**
|
|
7
|
+
* Create a new Memory instance
|
|
8
|
+
*/
|
|
9
|
+
constructor() {
|
|
10
|
+
this.messages = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get session history for a specific resource and thread
|
|
15
|
+
* @param {string} resourceId - The resource identifier
|
|
16
|
+
* @param {string} threadId - The thread identifier
|
|
17
|
+
* @param {number} limit - Maximum number of messages to retrieve (default: 10)
|
|
18
|
+
* @returns {Promise<Array>} Array of messages in the session
|
|
19
|
+
*/
|
|
20
|
+
async getSessionHistory(resourceId, threadId, limit = 10) {
|
|
21
|
+
// Filter messages by resourceId and threadId
|
|
22
|
+
const filtered = this.messages.filter(msg =>
|
|
23
|
+
msg.resourceId === resourceId &&
|
|
24
|
+
msg.threadId === threadId
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// Sort by timestamp (newest first) and apply limit
|
|
28
|
+
const sorted = filtered
|
|
29
|
+
.sort((a, b) => a.timestamp - b.timestamp)
|
|
30
|
+
.slice(0, limit);
|
|
31
|
+
|
|
32
|
+
return sorted;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Search messages by query
|
|
37
|
+
* @param {string} query - Search query
|
|
38
|
+
* @param {string} resourceId - The resource identifier to search within
|
|
39
|
+
* @param {number} limit - Maximum number of results (default: 5)
|
|
40
|
+
* @returns {Promise<Array>} Array of matching messages
|
|
41
|
+
*/
|
|
42
|
+
async search(query, resourceId, limit = 5) {
|
|
43
|
+
const queryLower = query.toLowerCase();
|
|
44
|
+
|
|
45
|
+
// Filter messages by resourceId and search content
|
|
46
|
+
const filtered = this.messages.filter(msg =>
|
|
47
|
+
msg.resourceId === resourceId &&
|
|
48
|
+
(msg.content.toLowerCase().includes(queryLower) ||
|
|
49
|
+
msg.role.toLowerCase().includes(queryLower))
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Sort by relevance (exact matches first) and apply limit
|
|
53
|
+
const sorted = filtered
|
|
54
|
+
.sort((a, b) => {
|
|
55
|
+
const aExact = a.content.toLowerCase() === queryLower ? 1 : 0;
|
|
56
|
+
const bExact = b.content.toLowerCase() === queryLower ? 1 : 0;
|
|
57
|
+
return bExact - aExact;
|
|
58
|
+
})
|
|
59
|
+
.slice(0, limit);
|
|
60
|
+
|
|
61
|
+
return sorted;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Save a message to memory
|
|
66
|
+
* @param {string} content - The message content
|
|
67
|
+
* @param {string} resourceId - The resource identifier
|
|
68
|
+
* @param {string} threadId - The thread identifier
|
|
69
|
+
* @param {string} role - The role (user, assistant, system, tool)
|
|
70
|
+
* @param {string} type - The message type
|
|
71
|
+
* @param {string} generationId - Optional generation identifier
|
|
72
|
+
* @param {string} remarks - Optional remarks
|
|
73
|
+
* @returns {Promise<Object>} The saved message object
|
|
74
|
+
*/
|
|
75
|
+
async save(content, resourceId, threadId, role, type, generationId = null, remarks = null) {
|
|
76
|
+
const message = {
|
|
77
|
+
id: this.generateId(),
|
|
78
|
+
content: content,
|
|
79
|
+
resourceId: resourceId,
|
|
80
|
+
threadId: threadId,
|
|
81
|
+
role: role,
|
|
82
|
+
type: type,
|
|
83
|
+
generationId: generationId,
|
|
84
|
+
remarks: remarks,
|
|
85
|
+
timestamp: Date.now()
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
this.messages.push(message);
|
|
89
|
+
|
|
90
|
+
return message;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a unique ID for messages
|
|
95
|
+
* @returns {string} Unique ID
|
|
96
|
+
*/
|
|
97
|
+
generateId() {
|
|
98
|
+
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get all messages (for debugging/testing)
|
|
103
|
+
* @returns {Array} All messages
|
|
104
|
+
*/
|
|
105
|
+
getAllMessages() {
|
|
106
|
+
return this.messages;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Clear all messages
|
|
111
|
+
*/
|
|
112
|
+
clear() {
|
|
113
|
+
this.messages = [];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { InMemory };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { InMemory } = require('./in-memory');
|
|
2
|
+
const { LanceMemory } = require('./lance-memory');
|
|
3
|
+
const { SQLiteMemory } = require('./sqlite-memory');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Memory factory for creating different types of memory instances
|
|
7
|
+
*/
|
|
8
|
+
const memory = {
|
|
9
|
+
/**
|
|
10
|
+
* Create a memory instance
|
|
11
|
+
* @param {Object} options - Memory configuration
|
|
12
|
+
* @param {string} options.type - Type of memory ('in-memory', 'lancedb', 'sqlite')
|
|
13
|
+
* @param {Object} options.config - Configuration for the specific memory type
|
|
14
|
+
* @returns {InMemory|LanceMemory|SQLiteMemory} Memory instance
|
|
15
|
+
*/
|
|
16
|
+
create(options = {}) {
|
|
17
|
+
const { type = 'in-memory', config = {} } = options;
|
|
18
|
+
|
|
19
|
+
switch (type.toLowerCase()) {
|
|
20
|
+
case 'in-memory':
|
|
21
|
+
return new InMemory(config);
|
|
22
|
+
|
|
23
|
+
case 'lancedb':
|
|
24
|
+
case 'lance':
|
|
25
|
+
return new LanceMemory(config);
|
|
26
|
+
|
|
27
|
+
case 'sqlite':
|
|
28
|
+
return new SQLiteMemory(config);
|
|
29
|
+
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unknown memory type: ${type}. Supported types: in-memory, lancedb, sqlite`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
module.exports = { memory, InMemory, LanceMemory, SQLiteMemory };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const { InMemory } = require('./in-memory');
|
|
2
|
+
const lancedb = require('@lancedb/lancedb');
|
|
3
|
+
const { pipeline } = require('@huggingface/transformers');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
// LanceDB schema will be inferred from first record
|
|
9
|
+
// but we ensure consistent types by providing proper values
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* LanceMemory class for managing conversation history with persistent storage
|
|
13
|
+
* Uses LanceDB for vector search and metadata storage
|
|
14
|
+
*/
|
|
15
|
+
class LanceMemory extends InMemory {
|
|
16
|
+
/**
|
|
17
|
+
* Create a new LanceMemory instance
|
|
18
|
+
* @param {Object} options - Memory configuration
|
|
19
|
+
* @param {string} options.db - Database name (default: 'lance')
|
|
20
|
+
* @param {string} options.dir - Data directory (default: './lance_data')
|
|
21
|
+
* @param {string} options.model - Embedding model (default: 'onnx-community/Qwen3-Embedding-0.6B-ONNX')
|
|
22
|
+
*/
|
|
23
|
+
constructor({ db = 'lance', dir = './lance_data', model = 'onnx-community/Qwen3-Embedding-0.6B-ONNX' } = {}) {
|
|
24
|
+
super();
|
|
25
|
+
this.db = db;
|
|
26
|
+
this.dir = dir;
|
|
27
|
+
this.model = model;
|
|
28
|
+
|
|
29
|
+
// Create directory if it doesn't exist
|
|
30
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
31
|
+
|
|
32
|
+
this.lanceTable = null;
|
|
33
|
+
this.extractor = null;
|
|
34
|
+
this.initialized = false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize the embedding model and LanceDB connection
|
|
39
|
+
*/
|
|
40
|
+
async init() {
|
|
41
|
+
if (this.initialized) return;
|
|
42
|
+
|
|
43
|
+
this.extractor = await pipeline(
|
|
44
|
+
'feature-extraction',
|
|
45
|
+
this.model,
|
|
46
|
+
{ dtype: 'q8', quantized: true }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Warm up the embedding model
|
|
50
|
+
for (let i = 0; i < 3; i++) {
|
|
51
|
+
await this.#getEmbedding("warming up warming up " + i + crypto.randomBytes(50).toString('hex'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log("LanceMemory embedding is up");
|
|
55
|
+
|
|
56
|
+
const db = await lancedb.connect(path.join(this.dir, `${this.db}.lance`));
|
|
57
|
+
try {
|
|
58
|
+
this.lanceTable = await db.openTable(this.db);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
this.lanceTable = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.initialized = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get embedding for a text
|
|
68
|
+
* @param {string} text - Text to embed
|
|
69
|
+
* @returns {Promise<Array>} Embedding vector
|
|
70
|
+
*/
|
|
71
|
+
async #getEmbedding(text) {
|
|
72
|
+
const output = await this.extractor(text, { pooling: 'mean', normalize: true });
|
|
73
|
+
return Array.from(output.data);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Save a message to memory
|
|
78
|
+
* @param {string} content - The message content
|
|
79
|
+
* @param {string} resourceId - The resource identifier
|
|
80
|
+
* @param {string} threadId - The thread identifier
|
|
81
|
+
* @param {string} role - The role (user, assistant, system, tool)
|
|
82
|
+
* @param {string} type - The message type
|
|
83
|
+
* @param {string} generationId - Optional generation identifier
|
|
84
|
+
* @param {string} remarks - Optional remarks
|
|
85
|
+
* @returns {Promise<Object>} The saved message object
|
|
86
|
+
*/
|
|
87
|
+
async save(content, resourceId, threadId, role, type, generationId = null, remarks = null) {
|
|
88
|
+
if (!this.initialized) await this.init();
|
|
89
|
+
|
|
90
|
+
// Generate unique ID
|
|
91
|
+
const id = `${resourceId}_${threadId}_${Date.now()}_${crypto.randomBytes(4).toString('hex')}`;
|
|
92
|
+
const createdAt = new Date().toISOString();
|
|
93
|
+
|
|
94
|
+
console.time('getEmbedding for save');
|
|
95
|
+
const vector = await this.#getEmbedding(content);
|
|
96
|
+
console.timeEnd('getEmbedding for save');
|
|
97
|
+
|
|
98
|
+
// LanceDB Store - ensure all fields are properly typed
|
|
99
|
+
const record = {
|
|
100
|
+
id,
|
|
101
|
+
resourceId,
|
|
102
|
+
threadId,
|
|
103
|
+
role,
|
|
104
|
+
type,
|
|
105
|
+
vector,
|
|
106
|
+
content,
|
|
107
|
+
generationId: generationId || '',
|
|
108
|
+
remarks: remarks || '',
|
|
109
|
+
created_at: createdAt
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (!this.lanceTable) {
|
|
113
|
+
const db = await lancedb.connect(path.join(this.dir, `${this.db}.lance`));
|
|
114
|
+
this.lanceTable = await db.createTable(this.db, [record]);
|
|
115
|
+
} else {
|
|
116
|
+
await this.lanceTable.add([record]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Also add to in-memory cache
|
|
120
|
+
const message = {
|
|
121
|
+
id,
|
|
122
|
+
content,
|
|
123
|
+
resourceId,
|
|
124
|
+
threadId,
|
|
125
|
+
role,
|
|
126
|
+
type,
|
|
127
|
+
generationId,
|
|
128
|
+
remarks,
|
|
129
|
+
timestamp: Date.now()
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
this.messages.push(message);
|
|
133
|
+
|
|
134
|
+
return message;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get session history for a specific resource and thread
|
|
139
|
+
* @param {string} resourceId - The resource identifier
|
|
140
|
+
* @param {string} threadId - The thread identifier
|
|
141
|
+
* @param {number} limit - Maximum number of messages to retrieve (default: 10)
|
|
142
|
+
* @returns {Promise<Array>} Array of messages in the session
|
|
143
|
+
*/
|
|
144
|
+
async getSessionHistory(resourceId, threadId, limit = 10) {
|
|
145
|
+
if (!this.initialized) await this.init();
|
|
146
|
+
if (!this.lanceTable) return [];
|
|
147
|
+
|
|
148
|
+
const results = await this.lanceTable
|
|
149
|
+
.query()
|
|
150
|
+
.where(`resourceId = "${resourceId}" AND threadId = "${threadId}"`)
|
|
151
|
+
.limit(limit)
|
|
152
|
+
.toArray();
|
|
153
|
+
|
|
154
|
+
return results.map(res => ({
|
|
155
|
+
id: res.id,
|
|
156
|
+
content: res.content,
|
|
157
|
+
resourceId: res.resourceId,
|
|
158
|
+
threadId: res.threadId,
|
|
159
|
+
role: res.role,
|
|
160
|
+
type: res.type,
|
|
161
|
+
generationId: res.generationId,
|
|
162
|
+
remarks: res.remarks,
|
|
163
|
+
timestamp: new Date(res.created_at).getTime()
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Search messages by query
|
|
169
|
+
* @param {string} query - Search query
|
|
170
|
+
* @param {string} resourceId - The resource identifier to search within
|
|
171
|
+
* @param {number} limit - Maximum number of results (default: 5)
|
|
172
|
+
* @returns {Promise<Array>} Array of matching messages
|
|
173
|
+
*/
|
|
174
|
+
async search(query, resourceId, limit = 5) {
|
|
175
|
+
if (!this.initialized) await this.init();
|
|
176
|
+
if (!this.lanceTable) return [];
|
|
177
|
+
|
|
178
|
+
console.time('getEmbedding for search');
|
|
179
|
+
const queryVector = await this.#getEmbedding(query);
|
|
180
|
+
console.timeEnd('getEmbedding for search');
|
|
181
|
+
|
|
182
|
+
const results = await this.lanceTable
|
|
183
|
+
.search(queryVector)
|
|
184
|
+
.where(`resourceId = "${resourceId}"`)
|
|
185
|
+
.limit(limit)
|
|
186
|
+
.toArray();
|
|
187
|
+
|
|
188
|
+
return results.map(res => ({
|
|
189
|
+
id: res.id,
|
|
190
|
+
content: res.content,
|
|
191
|
+
resourceId: res.resourceId,
|
|
192
|
+
threadId: res.threadId,
|
|
193
|
+
role: res.role,
|
|
194
|
+
type: res.type,
|
|
195
|
+
generationId: res.generationId,
|
|
196
|
+
remarks: res.remarks,
|
|
197
|
+
timestamp: new Date(res.created_at).getTime(),
|
|
198
|
+
distance: res._distance
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Clear all messages from memory and database
|
|
204
|
+
*/
|
|
205
|
+
async clear() {
|
|
206
|
+
this.messages = [];
|
|
207
|
+
|
|
208
|
+
// Drop LanceDB table
|
|
209
|
+
if (this.lanceTable) {
|
|
210
|
+
await this.lanceTable.delete('1=1');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Close the database connections and cleanup
|
|
216
|
+
*/
|
|
217
|
+
async close() {
|
|
218
|
+
if (this.extractor) {
|
|
219
|
+
await this.extractor.cleanup();
|
|
220
|
+
}
|
|
221
|
+
this.initialized = false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = { LanceMemory };
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
const { InMemory } = require('./in-memory');
|
|
2
|
+
const { pipeline } = require('@huggingface/transformers');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SQLiteMemory class for managing conversation history with SQLite storage
|
|
6
|
+
* Uses better-sqlite3, sqlite-vec, and HuggingFace transformers for vector search
|
|
7
|
+
*/
|
|
8
|
+
class SQLiteMemory extends InMemory {
|
|
9
|
+
/**
|
|
10
|
+
* Create a new SQLiteMemory instance
|
|
11
|
+
* @param {Object} options - Memory configuration
|
|
12
|
+
* @param {string} options.db - Database file path (default: './sqlite_memory.db')
|
|
13
|
+
* @param {string} options.model - Embedding model (default: 'onnx-community/Qwen3-Embedding-0.6B-ONNX')
|
|
14
|
+
*/
|
|
15
|
+
constructor({ db = './sqlite_memory.db', model = 'onnx-community/Qwen3-Embedding-0.6B-ONNX' } = {}) {
|
|
16
|
+
super();
|
|
17
|
+
this.dbPath = db;
|
|
18
|
+
this.model = model;
|
|
19
|
+
this.db = null;
|
|
20
|
+
this.extractor = null;
|
|
21
|
+
this.initialized = false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize the SQLite database, vector extensions, and embedding model
|
|
26
|
+
*/
|
|
27
|
+
async init() {
|
|
28
|
+
if (this.initialized) return;
|
|
29
|
+
|
|
30
|
+
// Initialize embedding model
|
|
31
|
+
this.extractor = await pipeline(
|
|
32
|
+
'feature-extraction',
|
|
33
|
+
this.model,
|
|
34
|
+
{ dtype: 'q8', quantized: true }
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Warm up the embedding model
|
|
38
|
+
for (let i = 0; i < 3; i++) {
|
|
39
|
+
await this._getEmbedding("warming up warming up " + i + Math.random().toString(36));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log("SQLiteMemory embedding is up");
|
|
43
|
+
|
|
44
|
+
const Database = require('better-sqlite3');
|
|
45
|
+
this.db = new Database(this.dbPath);
|
|
46
|
+
|
|
47
|
+
// Enable vector extension if available
|
|
48
|
+
try {
|
|
49
|
+
this.db.loadExtension(require('sqlite-vec'));
|
|
50
|
+
// Create vector table for embeddings
|
|
51
|
+
this.db.exec(`
|
|
52
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_messages USING vec0(
|
|
53
|
+
id TEXT PRIMARY KEY,
|
|
54
|
+
vector float[768]
|
|
55
|
+
);
|
|
56
|
+
`);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn('sqlite-vec extension not available, falling back to basic search');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Create main messages table
|
|
62
|
+
this.db.exec(`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
64
|
+
id TEXT PRIMARY KEY,
|
|
65
|
+
resourceId TEXT,
|
|
66
|
+
threadId TEXT,
|
|
67
|
+
role TEXT,
|
|
68
|
+
type TEXT,
|
|
69
|
+
content TEXT,
|
|
70
|
+
generationId TEXT,
|
|
71
|
+
remarks TEXT,
|
|
72
|
+
vector BLOB,
|
|
73
|
+
created_at TEXT
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_resource_thread ON messages(resourceId, threadId);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_created_at ON messages(created_at);
|
|
78
|
+
`);
|
|
79
|
+
|
|
80
|
+
// Prepare statements
|
|
81
|
+
this.insertStmt = this.db.prepare(`
|
|
82
|
+
INSERT OR REPLACE INTO messages (id, resourceId, threadId, role, type, content, generationId, remarks, vector, created_at)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
84
|
+
`);
|
|
85
|
+
|
|
86
|
+
this.selectStmt = this.db.prepare(`
|
|
87
|
+
SELECT * FROM messages
|
|
88
|
+
WHERE resourceId = ? AND threadId = ?
|
|
89
|
+
ORDER BY created_at DESC
|
|
90
|
+
LIMIT ?
|
|
91
|
+
`);
|
|
92
|
+
|
|
93
|
+
this.searchStmt = this.db.prepare(`
|
|
94
|
+
SELECT * FROM messages
|
|
95
|
+
WHERE resourceId = ?
|
|
96
|
+
ORDER BY created_at DESC
|
|
97
|
+
LIMIT ?
|
|
98
|
+
`);
|
|
99
|
+
|
|
100
|
+
this.deleteStmt = this.db.prepare('DELETE FROM messages');
|
|
101
|
+
|
|
102
|
+
// Vector search statements (if sqlite-vec is available)
|
|
103
|
+
if (this.db.loadExtension) {
|
|
104
|
+
try {
|
|
105
|
+
this.vectorInsertStmt = this.db.prepare(`
|
|
106
|
+
INSERT OR REPLACE INTO vec_messages (id, vector) VALUES (?, ?)
|
|
107
|
+
`);
|
|
108
|
+
|
|
109
|
+
this.vectorSearchStmt = this.db.prepare(`
|
|
110
|
+
SELECT id, distance
|
|
111
|
+
FROM vec_messages
|
|
112
|
+
WHERE vector MATCH ? AND k = ?
|
|
113
|
+
ORDER BY distance
|
|
114
|
+
`);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.warn('Vector search not available:', error.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.initialized = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get embedding for a text
|
|
125
|
+
* @param {string} text - Text to embed
|
|
126
|
+
* @returns {Promise<Array>} Embedding vector
|
|
127
|
+
*/
|
|
128
|
+
async _getEmbedding(text) {
|
|
129
|
+
const output = await this.extractor(text, { pooling: 'mean', normalize: true });
|
|
130
|
+
return Array.from(output.data);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Save a message to memory
|
|
135
|
+
* @param {string} content - The message content
|
|
136
|
+
* @param {string} resourceId - The resource identifier
|
|
137
|
+
* @param {string} threadId - The thread identifier
|
|
138
|
+
* @param {string} role - The role (user, assistant, system, tool)
|
|
139
|
+
* @param {string} type - The message type
|
|
140
|
+
* @param {string} generationId - Optional generation identifier
|
|
141
|
+
* @param {string} remarks - Optional remarks
|
|
142
|
+
* @returns {Promise<Object>} The saved message object
|
|
143
|
+
*/
|
|
144
|
+
async save(content, resourceId, threadId, role, type, generationId = null, remarks = null) {
|
|
145
|
+
if (!this.initialized) await this.init();
|
|
146
|
+
|
|
147
|
+
const id = `${resourceId}_${threadId}_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
148
|
+
const createdAt = new Date().toISOString();
|
|
149
|
+
|
|
150
|
+
console.time('getEmbedding for save');
|
|
151
|
+
const vector = await this._getEmbedding(content);
|
|
152
|
+
console.timeEnd('getEmbedding for save');
|
|
153
|
+
|
|
154
|
+
// Store vector as JSON for basic storage, and also in vector table if available
|
|
155
|
+
const vectorJson = JSON.stringify(vector);
|
|
156
|
+
|
|
157
|
+
this.insertStmt.run(
|
|
158
|
+
id, resourceId, threadId, role, type, content,
|
|
159
|
+
generationId || '', remarks || '', vectorJson, createdAt
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Store in vector table if available
|
|
163
|
+
if (this.vectorInsertStmt) {
|
|
164
|
+
try {
|
|
165
|
+
this.vectorInsertStmt.run(id, vector);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.warn('Failed to store vector:', error.message);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const message = {
|
|
172
|
+
id,
|
|
173
|
+
content,
|
|
174
|
+
resourceId,
|
|
175
|
+
threadId,
|
|
176
|
+
role,
|
|
177
|
+
type,
|
|
178
|
+
generationId,
|
|
179
|
+
remarks,
|
|
180
|
+
timestamp: Date.now()
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Also add to in-memory cache for faster access
|
|
184
|
+
this.messages.push(message);
|
|
185
|
+
|
|
186
|
+
return message;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get session history for a specific resource and thread
|
|
191
|
+
* @param {string} resourceId - The resource identifier
|
|
192
|
+
* @param {string} threadId - The thread identifier
|
|
193
|
+
* @param {number} limit - Maximum number of messages to retrieve (default: 10)
|
|
194
|
+
* @returns {Promise<Array>} Array of messages in the session
|
|
195
|
+
*/
|
|
196
|
+
async getSessionHistory(resourceId, threadId, limit = 10) {
|
|
197
|
+
if (!this.initialized) await this.init();
|
|
198
|
+
|
|
199
|
+
const rows = this.selectStmt.all(resourceId, threadId, limit);
|
|
200
|
+
return rows.map(row => ({
|
|
201
|
+
id: row.id,
|
|
202
|
+
content: row.content,
|
|
203
|
+
resourceId: row.resourceId,
|
|
204
|
+
threadId: row.threadId,
|
|
205
|
+
role: row.role,
|
|
206
|
+
type: row.type,
|
|
207
|
+
generationId: row.generationId,
|
|
208
|
+
remarks: row.remarks,
|
|
209
|
+
timestamp: new Date(row.created_at).getTime()
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Search messages by query using vector search when available
|
|
215
|
+
* @param {string} query - Search query
|
|
216
|
+
* @param {string} resourceId - The resource identifier to search within
|
|
217
|
+
* @param {number} limit - Maximum number of results (default: 5)
|
|
218
|
+
* @returns {Promise<Array>} Array of matching messages
|
|
219
|
+
*/
|
|
220
|
+
async search(query, resourceId, limit = 5) {
|
|
221
|
+
if (!this.initialized) await this.init();
|
|
222
|
+
|
|
223
|
+
// Try vector search first if available
|
|
224
|
+
if (this.vectorSearchStmt) {
|
|
225
|
+
try {
|
|
226
|
+
console.time('getEmbedding for search');
|
|
227
|
+
const queryVector = await this._getEmbedding(query);
|
|
228
|
+
console.timeEnd('getEmbedding for search');
|
|
229
|
+
|
|
230
|
+
const vectorResults = this.vectorSearchStmt.all(JSON.stringify(queryVector), limit);
|
|
231
|
+
|
|
232
|
+
if (vectorResults && vectorResults.length > 0) {
|
|
233
|
+
// Get full message details for the vector search results
|
|
234
|
+
const ids = vectorResults.map(r => r.id);
|
|
235
|
+
const messages = [];
|
|
236
|
+
|
|
237
|
+
for (const id of ids) {
|
|
238
|
+
const row = this.db.prepare('SELECT * FROM messages WHERE id = ?').get(id);
|
|
239
|
+
if (row) {
|
|
240
|
+
messages.push({
|
|
241
|
+
id: row.id,
|
|
242
|
+
content: row.content,
|
|
243
|
+
resourceId: row.resourceId,
|
|
244
|
+
threadId: row.threadId,
|
|
245
|
+
role: row.role,
|
|
246
|
+
type: row.type,
|
|
247
|
+
generationId: row.generationId,
|
|
248
|
+
remarks: row.remarks,
|
|
249
|
+
timestamp: new Date(row.created_at).getTime(),
|
|
250
|
+
distance: vectorResults.find(r => r.id === id)?.distance
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return messages;
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.warn('Vector search failed, falling back to text search:', error.message);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fallback to basic text search
|
|
263
|
+
const queryLower = query.toLowerCase();
|
|
264
|
+
const rows = this.searchStmt.all(resourceId, limit * 2); // Get more to filter
|
|
265
|
+
|
|
266
|
+
const filtered = rows.filter(row =>
|
|
267
|
+
row.content.toLowerCase().includes(queryLower) ||
|
|
268
|
+
row.role.toLowerCase().includes(queryLower)
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
return filtered.slice(0, limit).map(row => ({
|
|
272
|
+
id: row.id,
|
|
273
|
+
content: row.content,
|
|
274
|
+
resourceId: row.resourceId,
|
|
275
|
+
threadId: row.threadId,
|
|
276
|
+
role: row.role,
|
|
277
|
+
type: row.type,
|
|
278
|
+
generationId: row.generationId,
|
|
279
|
+
remarks: row.remarks,
|
|
280
|
+
timestamp: new Date(row.created_at).getTime()
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Clear all messages from memory and database
|
|
286
|
+
*/
|
|
287
|
+
async clear() {
|
|
288
|
+
this.messages = [];
|
|
289
|
+
|
|
290
|
+
if (this.db) {
|
|
291
|
+
this.deleteStmt.run();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Close the database connection and cleanup embedding model
|
|
297
|
+
*/
|
|
298
|
+
async close() {
|
|
299
|
+
if (this.extractor) {
|
|
300
|
+
await this.extractor.cleanup();
|
|
301
|
+
}
|
|
302
|
+
if (this.db) {
|
|
303
|
+
this.db.close();
|
|
304
|
+
}
|
|
305
|
+
this.initialized = false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = { SQLiteMemory };
|