@elizaos/plugin-local-embedding 2.0.0-alpha.10
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/dist/index.d.ts +9 -0
- package/dist/index.js +1886 -0
- package/dist/index.js.map +1 -0
- package/package.json +112 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1886 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import fs2 from "fs";
|
|
3
|
+
import os2 from "os";
|
|
4
|
+
import path2, { basename } from "path";
|
|
5
|
+
import { logger as logger5, ModelType } from "@elizaos/core";
|
|
6
|
+
import {
|
|
7
|
+
getLlama,
|
|
8
|
+
LlamaLogLevel
|
|
9
|
+
} from "node-llama-cpp";
|
|
10
|
+
|
|
11
|
+
// src/environment.ts
|
|
12
|
+
import { logger } from "@elizaos/core";
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var DEFAULT_EMBEDDING_MODEL = "bge-small-en-v1.5.Q4_K_M.gguf";
|
|
15
|
+
var configSchema = z.object({
|
|
16
|
+
LOCAL_EMBEDDING_MODEL: z.string().optional().default(DEFAULT_EMBEDDING_MODEL),
|
|
17
|
+
LOCAL_EMBEDDING_MODEL_REPO: z.string().optional(),
|
|
18
|
+
MODELS_DIR: z.string().optional(),
|
|
19
|
+
// Path for the models directory
|
|
20
|
+
CACHE_DIR: z.string().optional(),
|
|
21
|
+
// Path for the cache directory
|
|
22
|
+
LOCAL_EMBEDDING_DIMENSIONS: z.string().optional().transform((val) => {
|
|
23
|
+
if (!val || !val.trim()) return void 0;
|
|
24
|
+
const parsed = Number.parseInt(val, 10);
|
|
25
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
26
|
+
}),
|
|
27
|
+
LOCAL_EMBEDDING_CONTEXT_SIZE: z.string().optional().transform((val) => {
|
|
28
|
+
if (!val || !val.trim()) return void 0;
|
|
29
|
+
const parsed = Number.parseInt(val, 10);
|
|
30
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
31
|
+
}),
|
|
32
|
+
LOCAL_EMBEDDING_GPU_LAYERS: z.string().optional().default("0").transform((val) => {
|
|
33
|
+
if (val === "auto") return -1;
|
|
34
|
+
const num = parseInt(val, 10);
|
|
35
|
+
return Number.isNaN(num) ? 0 : num;
|
|
36
|
+
}),
|
|
37
|
+
LOCAL_EMBEDDING_USE_MMAP: z.string().optional().default("true").transform((val) => val === "true")
|
|
38
|
+
});
|
|
39
|
+
function validateConfig() {
|
|
40
|
+
try {
|
|
41
|
+
const configToParse = {
|
|
42
|
+
LOCAL_EMBEDDING_MODEL: process.env.LOCAL_EMBEDDING_MODEL,
|
|
43
|
+
LOCAL_EMBEDDING_MODEL_REPO: process.env.LOCAL_EMBEDDING_MODEL_REPO,
|
|
44
|
+
MODELS_DIR: process.env.MODELS_DIR,
|
|
45
|
+
// Read models directory path from env
|
|
46
|
+
CACHE_DIR: process.env.CACHE_DIR,
|
|
47
|
+
// Read cache directory path from env
|
|
48
|
+
LOCAL_EMBEDDING_DIMENSIONS: process.env.LOCAL_EMBEDDING_DIMENSIONS,
|
|
49
|
+
// Read embedding dimensions
|
|
50
|
+
LOCAL_EMBEDDING_CONTEXT_SIZE: process.env.LOCAL_EMBEDDING_CONTEXT_SIZE,
|
|
51
|
+
LOCAL_EMBEDDING_GPU_LAYERS: process.env.LOCAL_EMBEDDING_GPU_LAYERS,
|
|
52
|
+
LOCAL_EMBEDDING_USE_MMAP: process.env.LOCAL_EMBEDDING_USE_MMAP
|
|
53
|
+
};
|
|
54
|
+
logger.debug(
|
|
55
|
+
{
|
|
56
|
+
LOCAL_EMBEDDING_MODEL: configToParse.LOCAL_EMBEDDING_MODEL,
|
|
57
|
+
LOCAL_EMBEDDING_MODEL_REPO: configToParse.LOCAL_EMBEDDING_MODEL_REPO,
|
|
58
|
+
MODELS_DIR: configToParse.MODELS_DIR,
|
|
59
|
+
CACHE_DIR: configToParse.CACHE_DIR,
|
|
60
|
+
LOCAL_EMBEDDING_DIMENSIONS: configToParse.LOCAL_EMBEDDING_DIMENSIONS,
|
|
61
|
+
LOCAL_EMBEDDING_CONTEXT_SIZE: configToParse.LOCAL_EMBEDDING_CONTEXT_SIZE,
|
|
62
|
+
LOCAL_EMBEDDING_GPU_LAYERS: configToParse.LOCAL_EMBEDDING_GPU_LAYERS,
|
|
63
|
+
LOCAL_EMBEDDING_USE_MMAP: configToParse.LOCAL_EMBEDDING_USE_MMAP
|
|
64
|
+
},
|
|
65
|
+
"Validating configuration for local AI plugin from env:"
|
|
66
|
+
);
|
|
67
|
+
const validatedConfig = configSchema.parse(configToParse);
|
|
68
|
+
logger.info(validatedConfig, "Using local AI configuration:");
|
|
69
|
+
return validatedConfig;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
if (error instanceof z.ZodError) {
|
|
72
|
+
const errorMessages = error.issues.map((err) => `${err.path.join(".")}: ${err.message}`).join("\n");
|
|
73
|
+
logger.error(errorMessages, "Zod validation failed:");
|
|
74
|
+
throw new Error(`Configuration validation failed:
|
|
75
|
+
${errorMessages}`);
|
|
76
|
+
}
|
|
77
|
+
logger.error(
|
|
78
|
+
{
|
|
79
|
+
error: error instanceof Error ? error.message : String(error),
|
|
80
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
81
|
+
},
|
|
82
|
+
"Configuration validation failed:"
|
|
83
|
+
);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/types.ts
|
|
89
|
+
var MODEL_SPECS = {
|
|
90
|
+
small: {
|
|
91
|
+
name: "DeepHermes-3-Llama-3-3B-Preview-q4.gguf",
|
|
92
|
+
repo: "NousResearch/DeepHermes-3-Llama-3-3B-Preview-GGUF",
|
|
93
|
+
size: "3B",
|
|
94
|
+
quantization: "Q4_0",
|
|
95
|
+
contextSize: 8192,
|
|
96
|
+
tokenizer: {
|
|
97
|
+
name: "NousResearch/DeepHermes-3-Llama-3-3B-Preview",
|
|
98
|
+
type: "llama"
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
medium: {
|
|
102
|
+
name: "DeepHermes-3-Llama-3-8B-q4.gguf",
|
|
103
|
+
repo: "NousResearch/DeepHermes-3-Llama-3-8B-Preview-GGUF",
|
|
104
|
+
size: "8B",
|
|
105
|
+
quantization: "Q4_0",
|
|
106
|
+
contextSize: 8192,
|
|
107
|
+
tokenizer: {
|
|
108
|
+
name: "NousResearch/DeepHermes-3-Llama-3-8B-Preview",
|
|
109
|
+
type: "llama"
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
embedding: {
|
|
113
|
+
name: "bge-small-en-v1.5.Q4_K_M.gguf",
|
|
114
|
+
repo: "ChristianAzinn/bge-small-en-v1.5-gguf",
|
|
115
|
+
size: "133 MB",
|
|
116
|
+
quantization: "Q4_K_M",
|
|
117
|
+
contextSize: 512,
|
|
118
|
+
dimensions: 384,
|
|
119
|
+
tokenizer: {
|
|
120
|
+
name: "ChristianAzinn/bge-small-en-v1.5-gguf",
|
|
121
|
+
type: "llama"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
vision: {
|
|
125
|
+
name: "Florence-2-base-ft",
|
|
126
|
+
repo: "onnx-community/Florence-2-base-ft",
|
|
127
|
+
size: "0.23B",
|
|
128
|
+
modelId: "onnx-community/Florence-2-base-ft",
|
|
129
|
+
contextSize: 1024,
|
|
130
|
+
maxTokens: 256,
|
|
131
|
+
tasks: [
|
|
132
|
+
"CAPTION",
|
|
133
|
+
"DETAILED_CAPTION",
|
|
134
|
+
"MORE_DETAILED_CAPTION",
|
|
135
|
+
"CAPTION_TO_PHRASE_GROUNDING",
|
|
136
|
+
"OD",
|
|
137
|
+
"DENSE_REGION_CAPTION",
|
|
138
|
+
"REGION_PROPOSAL",
|
|
139
|
+
"OCR",
|
|
140
|
+
"OCR_WITH_REGION"
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
visionvl: {
|
|
144
|
+
name: "Qwen2.5-VL-3B-Instruct",
|
|
145
|
+
repo: "Qwen/Qwen2.5-VL-3B-Instruct",
|
|
146
|
+
size: "3B",
|
|
147
|
+
modelId: "Qwen/Qwen2.5-VL-3B-Instruct",
|
|
148
|
+
contextSize: 32768,
|
|
149
|
+
maxTokens: 1024,
|
|
150
|
+
tasks: [
|
|
151
|
+
"CAPTION",
|
|
152
|
+
"DETAILED_CAPTION",
|
|
153
|
+
"IMAGE_UNDERSTANDING",
|
|
154
|
+
"VISUAL_QUESTION_ANSWERING",
|
|
155
|
+
"OCR",
|
|
156
|
+
"VISUAL_LOCALIZATION",
|
|
157
|
+
"REGION_ANALYSIS"
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
tts: {
|
|
161
|
+
default: {
|
|
162
|
+
modelId: "Xenova/speecht5_tts",
|
|
163
|
+
defaultSampleRate: 16e3,
|
|
164
|
+
// SpeechT5 default
|
|
165
|
+
// Use the standard embedding URL
|
|
166
|
+
defaultSpeakerEmbeddingUrl: "https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/speaker_embeddings.bin"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/utils/downloadManager.ts
|
|
172
|
+
import fs from "fs";
|
|
173
|
+
import https from "https";
|
|
174
|
+
import path from "path";
|
|
175
|
+
import { logger as logger2 } from "@elizaos/core";
|
|
176
|
+
function parseContentLength(contentLength) {
|
|
177
|
+
if (!contentLength || Array.isArray(contentLength)) return null;
|
|
178
|
+
const parsed = Number.parseInt(contentLength, 10);
|
|
179
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
180
|
+
}
|
|
181
|
+
var DownloadManager = class _DownloadManager {
|
|
182
|
+
static instance = null;
|
|
183
|
+
cacheDir;
|
|
184
|
+
modelsDir;
|
|
185
|
+
// Track active downloads to prevent duplicates
|
|
186
|
+
activeDownloads = /* @__PURE__ */ new Map();
|
|
187
|
+
/**
|
|
188
|
+
* Creates a new instance of CacheManager.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} cacheDir - The directory path for caching data.
|
|
191
|
+
* @param {string} modelsDir - The directory path for model files.
|
|
192
|
+
*/
|
|
193
|
+
constructor(cacheDir, modelsDir) {
|
|
194
|
+
this.cacheDir = cacheDir;
|
|
195
|
+
this.modelsDir = modelsDir;
|
|
196
|
+
this.ensureCacheDirectory();
|
|
197
|
+
this.ensureModelsDirectory();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Returns the singleton instance of the DownloadManager class.
|
|
201
|
+
* If an instance does not already exist, it creates a new one using the provided cache directory and models directory.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} cacheDir - The directory where downloaded files are stored.
|
|
204
|
+
* @param {string} modelsDir - The directory where model files are stored.
|
|
205
|
+
* @returns {DownloadManager} The singleton instance of the DownloadManager class.
|
|
206
|
+
*/
|
|
207
|
+
static getInstance(cacheDir, modelsDir) {
|
|
208
|
+
if (!_DownloadManager.instance) {
|
|
209
|
+
_DownloadManager.instance = new _DownloadManager(cacheDir, modelsDir);
|
|
210
|
+
}
|
|
211
|
+
return _DownloadManager.instance;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Ensure that the cache directory exists.
|
|
215
|
+
*/
|
|
216
|
+
ensureCacheDirectory() {
|
|
217
|
+
if (!fs.existsSync(this.cacheDir)) {
|
|
218
|
+
fs.mkdirSync(this.cacheDir, { recursive: true });
|
|
219
|
+
logger2.debug("Created cache directory");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Ensure that the models directory exists. If it does not exist, create it.
|
|
224
|
+
*/
|
|
225
|
+
ensureModelsDirectory() {
|
|
226
|
+
logger2.debug("Ensuring models directory exists:", this.modelsDir);
|
|
227
|
+
if (!fs.existsSync(this.modelsDir)) {
|
|
228
|
+
fs.mkdirSync(this.modelsDir, { recursive: true });
|
|
229
|
+
logger2.debug("Created models directory");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Downloads a file from a given URL to a specified destination path asynchronously.
|
|
234
|
+
*
|
|
235
|
+
* @param {string} url - The URL from which to download the file.
|
|
236
|
+
* @param {string} destPath - The destination path where the downloaded file will be saved.
|
|
237
|
+
* @returns {Promise<void>} A Promise that resolves when the file download is completed successfully or rejects if an error occurs.
|
|
238
|
+
*/
|
|
239
|
+
async downloadFileInternal(url, destPath) {
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
logger2.info(`Starting download to: ${destPath}`);
|
|
242
|
+
const tempPath = `${destPath}.tmp`;
|
|
243
|
+
if (fs.existsSync(tempPath)) {
|
|
244
|
+
try {
|
|
245
|
+
logger2.warn(`Removing existing temporary file: ${tempPath}`);
|
|
246
|
+
fs.unlinkSync(tempPath);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
logger2.error(
|
|
249
|
+
`Failed to remove existing temporary file: ${err instanceof Error ? err.message : String(err)}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const request = https.get(
|
|
254
|
+
url,
|
|
255
|
+
{
|
|
256
|
+
headers: {
|
|
257
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
|
|
258
|
+
},
|
|
259
|
+
timeout: 3e5
|
|
260
|
+
// Increase timeout to 5 minutes
|
|
261
|
+
},
|
|
262
|
+
(response) => {
|
|
263
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
264
|
+
const redirectUrl = response.headers.location;
|
|
265
|
+
if (!redirectUrl) {
|
|
266
|
+
reject(new Error("Redirect location not found"));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
this.activeDownloads.delete(destPath);
|
|
270
|
+
this.downloadFile(redirectUrl, destPath).then(resolve).catch(reject);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (response.statusCode !== 200) {
|
|
274
|
+
reject(new Error(`Failed to download: ${response.statusCode}`));
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const expectedBytes = parseContentLength(response.headers["content-length"]);
|
|
278
|
+
const totalSize = expectedBytes ?? 0;
|
|
279
|
+
let downloadedSize = 0;
|
|
280
|
+
let lastLoggedPercent = 0;
|
|
281
|
+
const barLength = 30;
|
|
282
|
+
const fileName = path.basename(destPath);
|
|
283
|
+
logger2.info(`Downloading ${fileName}: ${"\u25B1".repeat(barLength)} 0%`);
|
|
284
|
+
const file = fs.createWriteStream(tempPath);
|
|
285
|
+
response.on("data", (chunk) => {
|
|
286
|
+
downloadedSize += chunk.length;
|
|
287
|
+
if (!totalSize) return;
|
|
288
|
+
const percent = Math.round(downloadedSize / totalSize * 100);
|
|
289
|
+
if (percent >= lastLoggedPercent + 5) {
|
|
290
|
+
const filledLength = Math.floor(downloadedSize / totalSize * barLength);
|
|
291
|
+
const progressBar = "\u25B0".repeat(filledLength) + "\u25B1".repeat(barLength - filledLength);
|
|
292
|
+
logger2.info(`Downloading ${fileName}: ${progressBar} ${percent}%`);
|
|
293
|
+
lastLoggedPercent = percent;
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
response.pipe(file);
|
|
297
|
+
file.on("finish", () => {
|
|
298
|
+
file.close(() => {
|
|
299
|
+
try {
|
|
300
|
+
const completedBar = "\u25B0".repeat(barLength);
|
|
301
|
+
logger2.info(`Downloading ${fileName}: ${completedBar} 100%`);
|
|
302
|
+
const destDir = path.dirname(destPath);
|
|
303
|
+
if (!fs.existsSync(destDir)) {
|
|
304
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
305
|
+
}
|
|
306
|
+
if (!fs.existsSync(tempPath)) {
|
|
307
|
+
reject(new Error(`Temporary file ${tempPath} does not exist`));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (expectedBytes !== null && downloadedSize !== expectedBytes) {
|
|
311
|
+
try {
|
|
312
|
+
fs.unlinkSync(tempPath);
|
|
313
|
+
} catch {
|
|
314
|
+
}
|
|
315
|
+
reject(
|
|
316
|
+
new Error(
|
|
317
|
+
`Downloaded file size mismatch for ${destPath}: expected ${expectedBytes} bytes, got ${downloadedSize}`
|
|
318
|
+
)
|
|
319
|
+
);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (fs.existsSync(destPath)) {
|
|
323
|
+
try {
|
|
324
|
+
const backupPath = `${destPath}.bak`;
|
|
325
|
+
fs.renameSync(destPath, backupPath);
|
|
326
|
+
logger2.info(`Created backup of existing file: ${backupPath}`);
|
|
327
|
+
fs.renameSync(tempPath, destPath);
|
|
328
|
+
if (fs.existsSync(backupPath)) {
|
|
329
|
+
fs.unlinkSync(backupPath);
|
|
330
|
+
logger2.info(`Removed backup file after successful update: ${backupPath}`);
|
|
331
|
+
}
|
|
332
|
+
} catch (moveErr) {
|
|
333
|
+
logger2.error(
|
|
334
|
+
`Error replacing file: ${moveErr instanceof Error ? moveErr.message : String(moveErr)}`
|
|
335
|
+
);
|
|
336
|
+
const backupPath = `${destPath}.bak`;
|
|
337
|
+
if (fs.existsSync(backupPath)) {
|
|
338
|
+
try {
|
|
339
|
+
fs.renameSync(backupPath, destPath);
|
|
340
|
+
logger2.info(`Restored from backup after failed update: ${backupPath}`);
|
|
341
|
+
} catch (restoreErr) {
|
|
342
|
+
logger2.error(
|
|
343
|
+
`Failed to restore from backup: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (fs.existsSync(tempPath)) {
|
|
348
|
+
try {
|
|
349
|
+
fs.unlinkSync(tempPath);
|
|
350
|
+
} catch (unlinkErr) {
|
|
351
|
+
logger2.error(
|
|
352
|
+
`Failed to clean up temp file: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
reject(moveErr);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
fs.renameSync(tempPath, destPath);
|
|
361
|
+
}
|
|
362
|
+
logger2.success(`Download of ${fileName} completed successfully`);
|
|
363
|
+
this.activeDownloads.delete(destPath);
|
|
364
|
+
resolve();
|
|
365
|
+
} catch (err) {
|
|
366
|
+
logger2.error(
|
|
367
|
+
`Error finalizing download: ${err instanceof Error ? err.message : String(err)}`
|
|
368
|
+
);
|
|
369
|
+
if (fs.existsSync(tempPath)) {
|
|
370
|
+
try {
|
|
371
|
+
fs.unlinkSync(tempPath);
|
|
372
|
+
} catch (unlinkErr) {
|
|
373
|
+
logger2.error(
|
|
374
|
+
`Failed to clean up temp file: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
this.activeDownloads.delete(destPath);
|
|
379
|
+
reject(err);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
file.on("error", (err) => {
|
|
384
|
+
logger2.error(`File write error: ${err instanceof Error ? err.message : String(err)}`);
|
|
385
|
+
file.close(() => {
|
|
386
|
+
if (fs.existsSync(tempPath)) {
|
|
387
|
+
try {
|
|
388
|
+
fs.unlinkSync(tempPath);
|
|
389
|
+
} catch (unlinkErr) {
|
|
390
|
+
logger2.error(
|
|
391
|
+
`Failed to clean up temp file after error: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
this.activeDownloads.delete(destPath);
|
|
396
|
+
reject(err);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
);
|
|
401
|
+
request.on("error", (err) => {
|
|
402
|
+
logger2.error(`Request error: ${err instanceof Error ? err.message : String(err)}`);
|
|
403
|
+
if (fs.existsSync(tempPath)) {
|
|
404
|
+
try {
|
|
405
|
+
fs.unlinkSync(tempPath);
|
|
406
|
+
} catch (unlinkErr) {
|
|
407
|
+
logger2.error(
|
|
408
|
+
`Failed to clean up temp file after request error: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
this.activeDownloads.delete(destPath);
|
|
413
|
+
reject(err);
|
|
414
|
+
});
|
|
415
|
+
request.on("timeout", () => {
|
|
416
|
+
logger2.error("Download timeout occurred");
|
|
417
|
+
request.destroy();
|
|
418
|
+
if (fs.existsSync(tempPath)) {
|
|
419
|
+
try {
|
|
420
|
+
fs.unlinkSync(tempPath);
|
|
421
|
+
} catch (unlinkErr) {
|
|
422
|
+
logger2.error(
|
|
423
|
+
`Failed to clean up temp file after timeout: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
this.activeDownloads.delete(destPath);
|
|
428
|
+
reject(new Error("Download timeout"));
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Asynchronously downloads a file from the specified URL to the destination path.
|
|
434
|
+
*
|
|
435
|
+
* @param {string} url - The URL of the file to download.
|
|
436
|
+
* @param {string} destPath - The destination path to save the downloaded file.
|
|
437
|
+
* @returns {Promise<void>} A Promise that resolves once the file has been successfully downloaded.
|
|
438
|
+
*/
|
|
439
|
+
async downloadFile(url, destPath) {
|
|
440
|
+
if (this.activeDownloads.has(destPath)) {
|
|
441
|
+
logger2.info(`Download for ${destPath} already in progress, waiting for it to complete...`);
|
|
442
|
+
const existingDownload = this.activeDownloads.get(destPath);
|
|
443
|
+
if (existingDownload) {
|
|
444
|
+
return existingDownload;
|
|
445
|
+
}
|
|
446
|
+
logger2.warn(
|
|
447
|
+
`Download for ${destPath} was marked as in progress but not found in tracking map`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
const downloadPromise = this.downloadFileInternal(url, destPath);
|
|
451
|
+
this.activeDownloads.set(destPath, downloadPromise);
|
|
452
|
+
try {
|
|
453
|
+
return await downloadPromise;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
this.activeDownloads.delete(destPath);
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Downloads a model specified by the modelSpec and saves it to the provided modelPath.
|
|
461
|
+
* If the model is successfully downloaded, returns true, otherwise returns false.
|
|
462
|
+
*
|
|
463
|
+
* @param {ModelSpec} modelSpec - The model specification containing repo and name.
|
|
464
|
+
* @param {string} modelPath - The path where the model will be saved.
|
|
465
|
+
* @returns {Promise<boolean>} - Indicates if the model was successfully downloaded or not.
|
|
466
|
+
*/
|
|
467
|
+
async downloadModel(modelSpec, modelPath, forceDownload = false) {
|
|
468
|
+
try {
|
|
469
|
+
logger2.info("Starting local model download...");
|
|
470
|
+
const modelDir = path.dirname(modelPath);
|
|
471
|
+
if (!fs.existsSync(modelDir)) {
|
|
472
|
+
logger2.info("Creating model directory:", modelDir);
|
|
473
|
+
fs.mkdirSync(modelDir, { recursive: true });
|
|
474
|
+
}
|
|
475
|
+
if (!fs.existsSync(modelPath) || forceDownload) {
|
|
476
|
+
if (forceDownload && fs.existsSync(modelPath)) {
|
|
477
|
+
logger2.warn("Force re-download requested; removing existing model file:", modelPath);
|
|
478
|
+
this.activeDownloads.delete(modelPath);
|
|
479
|
+
try {
|
|
480
|
+
fs.unlinkSync(modelPath);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
logger2.warn(
|
|
483
|
+
`Failed to remove existing model file before re-download: ${err instanceof Error ? err.message : String(err)}`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const attempts = [
|
|
488
|
+
{
|
|
489
|
+
description: "LFS URL with GGUF suffix",
|
|
490
|
+
url: `https://huggingface.co/${modelSpec.repo}/resolve/main/${modelSpec.name}?download=true`
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
description: "LFS URL without GGUF suffix",
|
|
494
|
+
url: `https://huggingface.co/${modelSpec.repo.replace("-GGUF", "")}/resolve/main/${modelSpec.name}?download=true`
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
description: "Standard URL with GGUF suffix",
|
|
498
|
+
url: `https://huggingface.co/${modelSpec.repo}/resolve/main/${modelSpec.name}`
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
description: "Standard URL without GGUF suffix",
|
|
502
|
+
url: `https://huggingface.co/${modelSpec.repo.replace("-GGUF", "")}/resolve/main/${modelSpec.name}`
|
|
503
|
+
}
|
|
504
|
+
];
|
|
505
|
+
let lastError = null;
|
|
506
|
+
let downloadSuccess = false;
|
|
507
|
+
for (const attempt of attempts) {
|
|
508
|
+
try {
|
|
509
|
+
logger2.info("Attempting model download:", {
|
|
510
|
+
description: attempt.description,
|
|
511
|
+
url: attempt.url,
|
|
512
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
513
|
+
});
|
|
514
|
+
await this.downloadFile(attempt.url, modelPath);
|
|
515
|
+
logger2.success(
|
|
516
|
+
`Model download complete: ${modelSpec.name} using ${attempt.description}`
|
|
517
|
+
);
|
|
518
|
+
downloadSuccess = true;
|
|
519
|
+
break;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
lastError = error;
|
|
522
|
+
logger2.warn("Model download attempt failed:", {
|
|
523
|
+
description: attempt.description,
|
|
524
|
+
error: error instanceof Error ? error.message : String(error),
|
|
525
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (!downloadSuccess) {
|
|
530
|
+
throw lastError || new Error("All download attempts failed");
|
|
531
|
+
}
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
logger2.info("Model already exists at:", modelPath);
|
|
535
|
+
return false;
|
|
536
|
+
} catch (error) {
|
|
537
|
+
logger2.error("Model download failed:", {
|
|
538
|
+
error: error instanceof Error ? error.message : String(error),
|
|
539
|
+
modelPath,
|
|
540
|
+
model: modelSpec.name
|
|
541
|
+
});
|
|
542
|
+
throw error;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Returns the cache directory path.
|
|
547
|
+
*
|
|
548
|
+
* @returns {string} The path of the cache directory.
|
|
549
|
+
*/
|
|
550
|
+
getCacheDir() {
|
|
551
|
+
return this.cacheDir;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Downloads a file from a given URL to a specified destination path.
|
|
555
|
+
*
|
|
556
|
+
* @param {string} url - The URL of the file to download.
|
|
557
|
+
* @param {string} destPath - The destination path where the file should be saved.
|
|
558
|
+
* @returns {Promise<void>} A Promise that resolves once the file has been downloaded.
|
|
559
|
+
*/
|
|
560
|
+
async downloadFromUrl(url, destPath) {
|
|
561
|
+
return this.downloadFile(url, destPath);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Ensures that the specified directory exists. If it does not exist, it will be created.
|
|
565
|
+
* @param {string} dirPath - The path of the directory to ensure existence of.
|
|
566
|
+
* @returns {void}
|
|
567
|
+
*/
|
|
568
|
+
ensureDirectoryExists(dirPath) {
|
|
569
|
+
if (!fs.existsSync(dirPath)) {
|
|
570
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
571
|
+
logger2.info(`Created directory: ${dirPath}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/utils/platform.ts
|
|
577
|
+
import { exec } from "child_process";
|
|
578
|
+
import os from "os";
|
|
579
|
+
import { promisify } from "util";
|
|
580
|
+
import { logger as logger3 } from "@elizaos/core";
|
|
581
|
+
var execAsync = promisify(exec);
|
|
582
|
+
var PlatformManager = class _PlatformManager {
|
|
583
|
+
static instance;
|
|
584
|
+
capabilities = null;
|
|
585
|
+
/**
|
|
586
|
+
* Private constructor method.
|
|
587
|
+
*/
|
|
588
|
+
constructor() {
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Get the singleton instance of the PlatformManager class
|
|
592
|
+
* @returns {PlatformManager} The instance of PlatformManager
|
|
593
|
+
*/
|
|
594
|
+
static getInstance() {
|
|
595
|
+
if (!_PlatformManager.instance) {
|
|
596
|
+
_PlatformManager.instance = new _PlatformManager();
|
|
597
|
+
}
|
|
598
|
+
return _PlatformManager.instance;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Asynchronous method to initialize platform detection.
|
|
602
|
+
*
|
|
603
|
+
* @returns {Promise<void>} Promise that resolves once platform detection is completed.
|
|
604
|
+
*/
|
|
605
|
+
async initialize() {
|
|
606
|
+
try {
|
|
607
|
+
logger3.info("Initializing platform detection...");
|
|
608
|
+
this.capabilities = await this.detectSystemCapabilities();
|
|
609
|
+
} catch (error) {
|
|
610
|
+
logger3.error("Platform detection failed", { error });
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Detects the system capabilities including platform, CPU information, GPU information,
|
|
616
|
+
* supported backends, and recommended model size.
|
|
617
|
+
*
|
|
618
|
+
* @returns {Promise<SystemCapabilities>} Details of the system capabilities including platform, CPU info, GPU info,
|
|
619
|
+
* recommended model size, and supported backends.
|
|
620
|
+
*/
|
|
621
|
+
async detectSystemCapabilities() {
|
|
622
|
+
const platform = process.platform;
|
|
623
|
+
const cpuInfo = this.getCPUInfo();
|
|
624
|
+
const gpu = await this.detectGPU();
|
|
625
|
+
const supportedBackends = await this.getSupportedBackends(platform, gpu);
|
|
626
|
+
const recommendedModelSize = this.getRecommendedModelSize(cpuInfo, gpu);
|
|
627
|
+
return {
|
|
628
|
+
platform,
|
|
629
|
+
cpu: cpuInfo,
|
|
630
|
+
gpu,
|
|
631
|
+
recommendedModelSize,
|
|
632
|
+
supportedBackends
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Returns information about the CPU and memory of the system.
|
|
637
|
+
* @returns {SystemCPU} The CPU information including model, number of cores, speed, architecture, and memory details.
|
|
638
|
+
*/
|
|
639
|
+
getCPUInfo() {
|
|
640
|
+
const cpus = os.cpus();
|
|
641
|
+
const totalMemory = os.totalmem();
|
|
642
|
+
const freeMemory = os.freemem();
|
|
643
|
+
return {
|
|
644
|
+
model: cpus[0].model,
|
|
645
|
+
cores: cpus.length,
|
|
646
|
+
speed: cpus[0].speed,
|
|
647
|
+
architecture: process.arch,
|
|
648
|
+
memory: {
|
|
649
|
+
total: totalMemory,
|
|
650
|
+
free: freeMemory
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Asynchronously detects the GPU information based on the current platform.
|
|
656
|
+
* @returns A promise that resolves with the GPU information if detection is successful, otherwise null.
|
|
657
|
+
*/
|
|
658
|
+
async detectGPU() {
|
|
659
|
+
const platform = process.platform;
|
|
660
|
+
try {
|
|
661
|
+
switch (platform) {
|
|
662
|
+
case "darwin":
|
|
663
|
+
return await this.detectMacGPU();
|
|
664
|
+
case "win32":
|
|
665
|
+
return await this.detectWindowsGPU();
|
|
666
|
+
case "linux":
|
|
667
|
+
return await this.detectLinuxGPU();
|
|
668
|
+
default:
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
} catch (error) {
|
|
672
|
+
logger3.error("GPU detection failed", { error });
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Asynchronously detects the GPU of a Mac system.
|
|
678
|
+
* @returns {Promise<SystemGPU>} A promise that resolves to an object representing the detected GPU.
|
|
679
|
+
*/
|
|
680
|
+
async detectMacGPU() {
|
|
681
|
+
try {
|
|
682
|
+
const { stdout } = await execAsync("sysctl -n machdep.cpu.brand_string");
|
|
683
|
+
const isAppleSilicon = stdout.toLowerCase().includes("apple");
|
|
684
|
+
if (isAppleSilicon) {
|
|
685
|
+
return {
|
|
686
|
+
name: "Apple Silicon",
|
|
687
|
+
type: "metal",
|
|
688
|
+
isAppleSilicon: true
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const { stdout: gpuInfo } = await execAsync("system_profiler SPDisplaysDataType");
|
|
692
|
+
return {
|
|
693
|
+
name: gpuInfo.split("Chipset Model:")[1]?.split("\n")[0]?.trim() || "Unknown GPU",
|
|
694
|
+
type: "metal",
|
|
695
|
+
isAppleSilicon: false
|
|
696
|
+
};
|
|
697
|
+
} catch (error) {
|
|
698
|
+
logger3.error("Mac GPU detection failed", { error });
|
|
699
|
+
return {
|
|
700
|
+
name: "Unknown Mac GPU",
|
|
701
|
+
type: "metal",
|
|
702
|
+
isAppleSilicon: false
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Detects the GPU in a Windows system and returns information about it.
|
|
708
|
+
*
|
|
709
|
+
* @returns {Promise<SystemGPU | null>} A promise that resolves with the detected GPU information or null if detection fails.
|
|
710
|
+
*/
|
|
711
|
+
async detectWindowsGPU() {
|
|
712
|
+
try {
|
|
713
|
+
const { stdout } = await execAsync("wmic path win32_VideoController get name");
|
|
714
|
+
const gpuName = stdout.split("\n")[1].trim();
|
|
715
|
+
if (gpuName.toLowerCase().includes("nvidia")) {
|
|
716
|
+
const { stdout: nvidiaInfo } = await execAsync(
|
|
717
|
+
"nvidia-smi --query-gpu=name,memory.total --format=csv,noheader"
|
|
718
|
+
);
|
|
719
|
+
const [name, memoryStr] = nvidiaInfo.split(",").map((s) => s.trim());
|
|
720
|
+
const memory = Number.parseInt(memoryStr, 10);
|
|
721
|
+
return {
|
|
722
|
+
name,
|
|
723
|
+
memory,
|
|
724
|
+
type: "cuda",
|
|
725
|
+
version: await this.getNvidiaDriverVersion()
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
name: gpuName,
|
|
730
|
+
type: "directml"
|
|
731
|
+
};
|
|
732
|
+
} catch (error) {
|
|
733
|
+
logger3.error("Windows GPU detection failed", { error });
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Asynchronously detects the GPU information for Linux systems.
|
|
739
|
+
* Tries to detect NVIDIA GPU first using 'nvidia-smi' command and if successful,
|
|
740
|
+
* returns the GPU name, memory size, type as 'cuda', and NVIDIA driver version.
|
|
741
|
+
* If NVIDIA detection fails, it falls back to checking for other GPUs using 'lspci | grep -i vga' command.
|
|
742
|
+
* If no GPU is detected, it returns null.
|
|
743
|
+
*
|
|
744
|
+
* @returns {Promise<SystemGPU | null>} The detected GPU information or null if detection fails.
|
|
745
|
+
*/
|
|
746
|
+
async detectLinuxGPU() {
|
|
747
|
+
try {
|
|
748
|
+
const { stdout } = await execAsync(
|
|
749
|
+
"nvidia-smi --query-gpu=name,memory.total --format=csv,noheader"
|
|
750
|
+
);
|
|
751
|
+
if (stdout) {
|
|
752
|
+
const [name, memoryStr] = stdout.split(",").map((s) => s.trim());
|
|
753
|
+
const memory = Number.parseInt(memoryStr, 10);
|
|
754
|
+
return {
|
|
755
|
+
name,
|
|
756
|
+
memory,
|
|
757
|
+
type: "cuda",
|
|
758
|
+
version: await this.getNvidiaDriverVersion()
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
} catch {
|
|
762
|
+
try {
|
|
763
|
+
const { stdout } = await execAsync("lspci | grep -i vga");
|
|
764
|
+
return {
|
|
765
|
+
name: stdout.split(":").pop()?.trim() || "Unknown GPU",
|
|
766
|
+
type: "none"
|
|
767
|
+
};
|
|
768
|
+
} catch (error) {
|
|
769
|
+
logger3.error("Linux GPU detection failed", { error });
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return null;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Asynchronously retrieves the driver version of the Nvidia GPU using the 'nvidia-smi' command.
|
|
777
|
+
*
|
|
778
|
+
* @returns A promise that resolves with the driver version as a string, or 'unknown' if an error occurs.
|
|
779
|
+
*/
|
|
780
|
+
async getNvidiaDriverVersion() {
|
|
781
|
+
try {
|
|
782
|
+
const { stdout } = await execAsync(
|
|
783
|
+
"nvidia-smi --query-gpu=driver_version --format=csv,noheader"
|
|
784
|
+
);
|
|
785
|
+
return stdout.trim();
|
|
786
|
+
} catch {
|
|
787
|
+
return "unknown";
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Retrieves the supported backends based on the platform and GPU type.
|
|
792
|
+
* @param {NodeJS.Platform} platform - The platform on which the code is running.
|
|
793
|
+
* @param {SystemGPU | null} gpu - The GPU information, if available.
|
|
794
|
+
* @returns {Promise<Array<"cuda" | "metal" | "directml" | "cpu">>} - An array of supported backends including 'cuda', 'metal', 'directml', and 'cpu'.
|
|
795
|
+
*/
|
|
796
|
+
async getSupportedBackends(platform, gpu) {
|
|
797
|
+
const backends = ["cpu"];
|
|
798
|
+
if (gpu) {
|
|
799
|
+
switch (platform) {
|
|
800
|
+
case "darwin":
|
|
801
|
+
backends.push("metal");
|
|
802
|
+
break;
|
|
803
|
+
case "win32":
|
|
804
|
+
if (gpu.type === "cuda") {
|
|
805
|
+
backends.push("cuda");
|
|
806
|
+
}
|
|
807
|
+
backends.push("directml");
|
|
808
|
+
break;
|
|
809
|
+
case "linux":
|
|
810
|
+
if (gpu.type === "cuda") {
|
|
811
|
+
backends.push("cuda");
|
|
812
|
+
}
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return backends;
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Determines the recommended model size based on the system's CPU and GPU.
|
|
820
|
+
* @param {SystemCPU} cpu - The system's CPU.
|
|
821
|
+
* @param {SystemGPU | null} gpu - The system's GPU, if available.
|
|
822
|
+
* @returns {"small" | "medium" | "large"} - The recommended model size ("small", "medium", or "large").
|
|
823
|
+
*/
|
|
824
|
+
getRecommendedModelSize(cpu, gpu) {
|
|
825
|
+
if (gpu?.isAppleSilicon) {
|
|
826
|
+
return cpu.memory.total > 16 * 1024 * 1024 * 1024 ? "medium" : "small";
|
|
827
|
+
}
|
|
828
|
+
if (gpu?.type === "cuda") {
|
|
829
|
+
const gpuMemGB = (gpu.memory || 0) / 1024;
|
|
830
|
+
if (gpuMemGB >= 16) return "large";
|
|
831
|
+
if (gpuMemGB >= 8) return "medium";
|
|
832
|
+
}
|
|
833
|
+
if (cpu.memory.total > 32 * 1024 * 1024 * 1024) return "medium";
|
|
834
|
+
return "small";
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Returns the SystemCapabilities of the PlatformManager.
|
|
838
|
+
*
|
|
839
|
+
* @returns {SystemCapabilities} The SystemCapabilities of the PlatformManager.
|
|
840
|
+
* @throws {Error} if PlatformManager is not initialized.
|
|
841
|
+
*/
|
|
842
|
+
getCapabilities() {
|
|
843
|
+
if (!this.capabilities) {
|
|
844
|
+
throw new Error("PlatformManager not initialized");
|
|
845
|
+
}
|
|
846
|
+
return this.capabilities;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Checks if the device's GPU is Apple Silicon.
|
|
850
|
+
* @returns {boolean} True if the GPU is Apple Silicon, false otherwise.
|
|
851
|
+
*/
|
|
852
|
+
isAppleSilicon() {
|
|
853
|
+
return !!this.capabilities?.gpu?.isAppleSilicon;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Checks if the current device has GPU support.
|
|
857
|
+
* @returns {boolean} - Returns true if the device has GPU support, false otherwise.
|
|
858
|
+
*/
|
|
859
|
+
hasGPUSupport() {
|
|
860
|
+
return !!this.capabilities?.gpu;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Checks if the system supports CUDA GPU for processing.
|
|
864
|
+
*
|
|
865
|
+
* @returns {boolean} True if the system supports CUDA, false otherwise.
|
|
866
|
+
*/
|
|
867
|
+
supportsCUDA() {
|
|
868
|
+
return this.capabilities?.gpu?.type === "cuda";
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Check if the device supports Metal API for rendering graphics.
|
|
872
|
+
* @returns {boolean} True if the device supports Metal, false otherwise.
|
|
873
|
+
*/
|
|
874
|
+
supportsMetal() {
|
|
875
|
+
return this.capabilities?.gpu?.type === "metal";
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Check if the device supports DirectML for GPU acceleration.
|
|
879
|
+
*
|
|
880
|
+
* @returns {boolean} True if the device supports DirectML, false otherwise.
|
|
881
|
+
*/
|
|
882
|
+
supportsDirectML() {
|
|
883
|
+
return this.capabilities?.gpu?.type === "directml";
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Get the recommended backend for computation based on the available capabilities.
|
|
887
|
+
* @returns {"cuda" | "metal" | "directml" | "cpu"} The recommended backend for computation.
|
|
888
|
+
* @throws {Error} Throws an error if PlatformManager is not initialized.
|
|
889
|
+
*/
|
|
890
|
+
getRecommendedBackend() {
|
|
891
|
+
if (!this.capabilities) {
|
|
892
|
+
throw new Error("PlatformManager not initialized");
|
|
893
|
+
}
|
|
894
|
+
const { gpu, supportedBackends } = this.capabilities;
|
|
895
|
+
if (gpu?.type === "cuda") return "cuda";
|
|
896
|
+
if (gpu?.type === "metal") return "metal";
|
|
897
|
+
if (supportedBackends.includes("directml")) return "directml";
|
|
898
|
+
return "cpu";
|
|
899
|
+
}
|
|
900
|
+
};
|
|
901
|
+
var getPlatformManager = () => {
|
|
902
|
+
return PlatformManager.getInstance();
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
// src/utils/tokenizerManager.ts
|
|
906
|
+
import { logger as logger4 } from "@elizaos/core";
|
|
907
|
+
import { AutoTokenizer } from "@huggingface/transformers";
|
|
908
|
+
var TokenizerManager = class _TokenizerManager {
|
|
909
|
+
static instance = null;
|
|
910
|
+
tokenizers;
|
|
911
|
+
cacheDir;
|
|
912
|
+
modelsDir;
|
|
913
|
+
/**
|
|
914
|
+
* Constructor for creating a new instance of the class.
|
|
915
|
+
*
|
|
916
|
+
* @param {string} cacheDir - The directory for caching data.
|
|
917
|
+
* @param {string} modelsDir - The directory for storing models.
|
|
918
|
+
*/
|
|
919
|
+
constructor(cacheDir, modelsDir) {
|
|
920
|
+
this.tokenizers = /* @__PURE__ */ new Map();
|
|
921
|
+
this.cacheDir = cacheDir;
|
|
922
|
+
this.modelsDir = modelsDir;
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* Get the singleton instance of TokenizerManager class. If the instance does not exist, it will create a new one.
|
|
926
|
+
*
|
|
927
|
+
* @param {string} cacheDir - The directory to cache the tokenizer models.
|
|
928
|
+
* @param {string} modelsDir - The directory where tokenizer models are stored.
|
|
929
|
+
* @returns {TokenizerManager} The singleton instance of TokenizerManager.
|
|
930
|
+
*/
|
|
931
|
+
static getInstance(cacheDir, modelsDir) {
|
|
932
|
+
if (!_TokenizerManager.instance) {
|
|
933
|
+
_TokenizerManager.instance = new _TokenizerManager(cacheDir, modelsDir);
|
|
934
|
+
}
|
|
935
|
+
return _TokenizerManager.instance;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Asynchronously loads a tokenizer based on the provided ModelSpec configuration.
|
|
939
|
+
*
|
|
940
|
+
* @param {ModelSpec} modelConfig - The configuration object for the model to load the tokenizer for.
|
|
941
|
+
* @returns {Promise<PreTrainedTokenizer>} - A promise that resolves to the loaded tokenizer.
|
|
942
|
+
*/
|
|
943
|
+
async loadTokenizer(modelConfig) {
|
|
944
|
+
try {
|
|
945
|
+
const tokenizerKey = `${modelConfig.tokenizer.type}-${modelConfig.tokenizer.name}`;
|
|
946
|
+
logger4.info("Loading tokenizer:", {
|
|
947
|
+
key: tokenizerKey,
|
|
948
|
+
name: modelConfig.tokenizer.name,
|
|
949
|
+
type: modelConfig.tokenizer.type,
|
|
950
|
+
modelsDir: this.modelsDir,
|
|
951
|
+
cacheDir: this.cacheDir
|
|
952
|
+
});
|
|
953
|
+
if (this.tokenizers.has(tokenizerKey)) {
|
|
954
|
+
logger4.info("Using cached tokenizer:", { key: tokenizerKey });
|
|
955
|
+
const cachedTokenizer = this.tokenizers.get(tokenizerKey);
|
|
956
|
+
if (!cachedTokenizer) {
|
|
957
|
+
throw new Error(`Tokenizer ${tokenizerKey} exists in map but returned undefined`);
|
|
958
|
+
}
|
|
959
|
+
return cachedTokenizer;
|
|
960
|
+
}
|
|
961
|
+
const fs3 = await import("fs");
|
|
962
|
+
if (!fs3.existsSync(this.modelsDir)) {
|
|
963
|
+
logger4.warn("Models directory does not exist, creating it:", this.modelsDir);
|
|
964
|
+
fs3.mkdirSync(this.modelsDir, { recursive: true });
|
|
965
|
+
}
|
|
966
|
+
logger4.info(
|
|
967
|
+
"Initializing new tokenizer from HuggingFace with models directory:",
|
|
968
|
+
this.modelsDir
|
|
969
|
+
);
|
|
970
|
+
try {
|
|
971
|
+
const tokenizer = await AutoTokenizer.from_pretrained(modelConfig.tokenizer.name, {
|
|
972
|
+
cache_dir: this.modelsDir,
|
|
973
|
+
local_files_only: false
|
|
974
|
+
});
|
|
975
|
+
this.tokenizers.set(tokenizerKey, tokenizer);
|
|
976
|
+
logger4.success("Tokenizer loaded successfully:", { key: tokenizerKey });
|
|
977
|
+
return tokenizer;
|
|
978
|
+
} catch (tokenizeError) {
|
|
979
|
+
logger4.error("Failed to load tokenizer from HuggingFace:", {
|
|
980
|
+
error: tokenizeError instanceof Error ? tokenizeError.message : String(tokenizeError),
|
|
981
|
+
stack: tokenizeError instanceof Error ? tokenizeError.stack : void 0,
|
|
982
|
+
tokenizer: modelConfig.tokenizer.name,
|
|
983
|
+
modelsDir: this.modelsDir
|
|
984
|
+
});
|
|
985
|
+
logger4.info("Retrying tokenizer loading...");
|
|
986
|
+
const tokenizer = await AutoTokenizer.from_pretrained(modelConfig.tokenizer.name, {
|
|
987
|
+
cache_dir: this.modelsDir,
|
|
988
|
+
local_files_only: false
|
|
989
|
+
});
|
|
990
|
+
this.tokenizers.set(tokenizerKey, tokenizer);
|
|
991
|
+
logger4.success("Tokenizer loaded successfully on retry:", {
|
|
992
|
+
key: tokenizerKey
|
|
993
|
+
});
|
|
994
|
+
return tokenizer;
|
|
995
|
+
}
|
|
996
|
+
} catch (error) {
|
|
997
|
+
logger4.error("Failed to load tokenizer:", {
|
|
998
|
+
error: error instanceof Error ? error.message : String(error),
|
|
999
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1000
|
+
model: modelConfig.name,
|
|
1001
|
+
tokenizer: modelConfig.tokenizer.name,
|
|
1002
|
+
modelsDir: this.modelsDir
|
|
1003
|
+
});
|
|
1004
|
+
throw error;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Encodes the given text using the specified tokenizer model configuration.
|
|
1009
|
+
*
|
|
1010
|
+
* @param {string} text - The text to encode.
|
|
1011
|
+
* @param {ModelSpec} modelConfig - The configuration for the model tokenizer.
|
|
1012
|
+
* @returns {Promise<number[]>} - An array of integers representing the encoded text.
|
|
1013
|
+
* @throws {Error} - If the text encoding fails, an error is thrown.
|
|
1014
|
+
*/
|
|
1015
|
+
async encode(text, modelConfig) {
|
|
1016
|
+
try {
|
|
1017
|
+
logger4.info("Encoding text with tokenizer:", {
|
|
1018
|
+
length: text.length,
|
|
1019
|
+
tokenizer: modelConfig.tokenizer.name
|
|
1020
|
+
});
|
|
1021
|
+
const tokenizer = await this.loadTokenizer(modelConfig);
|
|
1022
|
+
logger4.info("Tokenizer loaded, encoding text...");
|
|
1023
|
+
const encoded = await tokenizer.encode(text, {
|
|
1024
|
+
add_special_tokens: true,
|
|
1025
|
+
return_token_type_ids: false
|
|
1026
|
+
});
|
|
1027
|
+
logger4.info("Text encoded successfully:", {
|
|
1028
|
+
tokenCount: encoded.length,
|
|
1029
|
+
tokenizer: modelConfig.tokenizer.name
|
|
1030
|
+
});
|
|
1031
|
+
return encoded;
|
|
1032
|
+
} catch (error) {
|
|
1033
|
+
logger4.error("Text encoding failed:", {
|
|
1034
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1035
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1036
|
+
textLength: text.length,
|
|
1037
|
+
tokenizer: modelConfig.tokenizer.name,
|
|
1038
|
+
modelsDir: this.modelsDir
|
|
1039
|
+
});
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* Asynchronously decodes an array of tokens using a tokenizer based on the provided ModelSpec.
|
|
1045
|
+
*
|
|
1046
|
+
* @param {number[]} tokens - The array of tokens to be decoded.
|
|
1047
|
+
* @param {ModelSpec} modelConfig - The ModelSpec object containing information about the model and tokenizer to be used.
|
|
1048
|
+
* @returns {Promise<string>} - A Promise that resolves with the decoded text.
|
|
1049
|
+
* @throws {Error} - If an error occurs during token decoding.
|
|
1050
|
+
*/
|
|
1051
|
+
async decode(tokens, modelConfig) {
|
|
1052
|
+
try {
|
|
1053
|
+
logger4.info("Decoding tokens with tokenizer:", {
|
|
1054
|
+
count: tokens.length,
|
|
1055
|
+
tokenizer: modelConfig.tokenizer.name
|
|
1056
|
+
});
|
|
1057
|
+
const tokenizer = await this.loadTokenizer(modelConfig);
|
|
1058
|
+
logger4.info("Tokenizer loaded, decoding tokens...");
|
|
1059
|
+
const decoded = await tokenizer.decode(tokens, {
|
|
1060
|
+
skip_special_tokens: true,
|
|
1061
|
+
clean_up_tokenization_spaces: true
|
|
1062
|
+
});
|
|
1063
|
+
logger4.info("Tokens decoded successfully:", {
|
|
1064
|
+
textLength: decoded.length,
|
|
1065
|
+
tokenizer: modelConfig.tokenizer.name
|
|
1066
|
+
});
|
|
1067
|
+
return decoded;
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
logger4.error("Token decoding failed:", {
|
|
1070
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1071
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1072
|
+
tokenCount: tokens.length,
|
|
1073
|
+
tokenizer: modelConfig.tokenizer.name,
|
|
1074
|
+
modelsDir: this.modelsDir
|
|
1075
|
+
});
|
|
1076
|
+
throw error;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
// src/index.ts
|
|
1082
|
+
var CORRUPTED_MODEL_ERROR_SIGNATURES = [
|
|
1083
|
+
"data is not within the file bounds",
|
|
1084
|
+
"failed to load model",
|
|
1085
|
+
"model is corrupted",
|
|
1086
|
+
"data of tensor",
|
|
1087
|
+
"is out of bounds"
|
|
1088
|
+
];
|
|
1089
|
+
var CONTEXT_LIMIT_ERROR_SIGNATURES = [
|
|
1090
|
+
"input is longer than the context size",
|
|
1091
|
+
"context size",
|
|
1092
|
+
"too many tokens",
|
|
1093
|
+
"exceeds context"
|
|
1094
|
+
];
|
|
1095
|
+
var NODE_LLAMA_NOISY_LOAD_ERROR_PATTERNS = [
|
|
1096
|
+
"llama_model_load:",
|
|
1097
|
+
"llama_model_load_from_file_impl: failed to load model"
|
|
1098
|
+
];
|
|
1099
|
+
var MIN_EMBEDDING_RETRY_TEXT_LENGTH = 1;
|
|
1100
|
+
var EMBEDDING_MODEL_HINTS = [
|
|
1101
|
+
{
|
|
1102
|
+
pattern: /nomic-embed-text-v1\.5/i,
|
|
1103
|
+
repo: "nomic-ai/nomic-embed-text-v1.5-GGUF",
|
|
1104
|
+
dimensions: 768,
|
|
1105
|
+
contextSize: 8192
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
pattern: /bge-small-en-v1\.5/i,
|
|
1109
|
+
repo: "ChristianAzinn/bge-small-en-v1.5-gguf",
|
|
1110
|
+
dimensions: 384,
|
|
1111
|
+
contextSize: 512
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
pattern: /e5-mistral-7b/i,
|
|
1115
|
+
repo: "dranger003/e5-mistral-7b-instruct-GGUF",
|
|
1116
|
+
dimensions: 4096,
|
|
1117
|
+
contextSize: 32768
|
|
1118
|
+
}
|
|
1119
|
+
];
|
|
1120
|
+
function getErrorMessage(error) {
|
|
1121
|
+
if (error instanceof Error) return error.message;
|
|
1122
|
+
if (typeof error === "string") return error;
|
|
1123
|
+
return String(error);
|
|
1124
|
+
}
|
|
1125
|
+
function isCorruptedModelLoadError(error) {
|
|
1126
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
1127
|
+
return CORRUPTED_MODEL_ERROR_SIGNATURES.some((signature) => message.includes(signature));
|
|
1128
|
+
}
|
|
1129
|
+
function isContextLimitError(error) {
|
|
1130
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
1131
|
+
return CONTEXT_LIMIT_ERROR_SIGNATURES.some((signature) => message.includes(signature));
|
|
1132
|
+
}
|
|
1133
|
+
function shouldSuppressNodeLlamaLoadError(message) {
|
|
1134
|
+
const lower = message.toLowerCase();
|
|
1135
|
+
return NODE_LLAMA_NOISY_LOAD_ERROR_PATTERNS.some((pattern) => lower.includes(pattern));
|
|
1136
|
+
}
|
|
1137
|
+
function shrinkEmbeddingInput(text) {
|
|
1138
|
+
if (text.length <= MIN_EMBEDDING_RETRY_TEXT_LENGTH) return text;
|
|
1139
|
+
const nextLength = Math.max(MIN_EMBEDDING_RETRY_TEXT_LENGTH, Math.floor(text.length / 2));
|
|
1140
|
+
return text.slice(0, nextLength);
|
|
1141
|
+
}
|
|
1142
|
+
function inferEmbeddingModelHint(modelName) {
|
|
1143
|
+
const match = EMBEDDING_MODEL_HINTS.find((hint) => hint.pattern.test(modelName));
|
|
1144
|
+
return match ?? null;
|
|
1145
|
+
}
|
|
1146
|
+
function resolveEmbeddingModelSpec(config, fallback) {
|
|
1147
|
+
const modelName = config.LOCAL_EMBEDDING_MODEL || fallback.name;
|
|
1148
|
+
const hint = inferEmbeddingModelHint(modelName);
|
|
1149
|
+
return {
|
|
1150
|
+
...fallback,
|
|
1151
|
+
name: modelName,
|
|
1152
|
+
repo: config.LOCAL_EMBEDDING_MODEL_REPO?.trim() || hint?.repo || fallback.repo,
|
|
1153
|
+
dimensions: config.LOCAL_EMBEDDING_DIMENSIONS ?? hint?.dimensions ?? fallback.dimensions,
|
|
1154
|
+
contextSize: config.LOCAL_EMBEDDING_CONTEXT_SIZE ?? hint?.contextSize ?? fallback.contextSize
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function readMagicHeader(filePath) {
|
|
1158
|
+
try {
|
|
1159
|
+
const fd = fs2.openSync(filePath, "r");
|
|
1160
|
+
try {
|
|
1161
|
+
const header = Buffer.alloc(4);
|
|
1162
|
+
const bytesRead = fs2.readSync(fd, header, 0, header.length, 0);
|
|
1163
|
+
return { bytesRead, magic: header.toString("ascii", 0, 4) };
|
|
1164
|
+
} finally {
|
|
1165
|
+
fs2.closeSync(fd);
|
|
1166
|
+
}
|
|
1167
|
+
} catch {
|
|
1168
|
+
return { bytesRead: 0, magic: "" };
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function isValidGgufFile(filePath) {
|
|
1172
|
+
const { bytesRead, magic } = readMagicHeader(filePath);
|
|
1173
|
+
return bytesRead === 4 && magic === "GGUF";
|
|
1174
|
+
}
|
|
1175
|
+
function safeUnlink(filePath) {
|
|
1176
|
+
if (!filePath || !fs2.existsSync(filePath)) return;
|
|
1177
|
+
try {
|
|
1178
|
+
fs2.unlinkSync(filePath);
|
|
1179
|
+
} catch (err) {
|
|
1180
|
+
logger5.warn(
|
|
1181
|
+
`Failed to remove model file ${filePath}: ${err instanceof Error ? err.message : String(err)}`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
var LocalAIManager = class _LocalAIManager {
|
|
1186
|
+
static instance = null;
|
|
1187
|
+
llama;
|
|
1188
|
+
embeddingModel;
|
|
1189
|
+
embeddingContext;
|
|
1190
|
+
modelPath;
|
|
1191
|
+
mediumModelPath;
|
|
1192
|
+
embeddingModelPath;
|
|
1193
|
+
cacheDir;
|
|
1194
|
+
tokenizerManager;
|
|
1195
|
+
downloadManager;
|
|
1196
|
+
activeModelConfig;
|
|
1197
|
+
embeddingModelConfig;
|
|
1198
|
+
config = null;
|
|
1199
|
+
// Store validated config
|
|
1200
|
+
// Initialization state flag
|
|
1201
|
+
embeddingInitialized = false;
|
|
1202
|
+
environmentInitialized = false;
|
|
1203
|
+
// Add flag for environment initialization
|
|
1204
|
+
// Initialization promises to prevent duplicate initialization
|
|
1205
|
+
embeddingInitializingPromise = null;
|
|
1206
|
+
environmentInitializingPromise = null;
|
|
1207
|
+
// Add promise for environment
|
|
1208
|
+
modelsDir;
|
|
1209
|
+
/**
|
|
1210
|
+
* Private constructor function to initialize base managers and paths.
|
|
1211
|
+
* Model paths are set after environment initialization.
|
|
1212
|
+
*/
|
|
1213
|
+
constructor() {
|
|
1214
|
+
this.config = validateConfig();
|
|
1215
|
+
this._setupCacheDir();
|
|
1216
|
+
this.activeModelConfig = MODEL_SPECS.small;
|
|
1217
|
+
this.embeddingModelConfig = MODEL_SPECS.embedding;
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Post-validation initialization steps that require config to be set.
|
|
1221
|
+
* Called after config validation in initializeEnvironment.
|
|
1222
|
+
*/
|
|
1223
|
+
_postValidateInit() {
|
|
1224
|
+
this._setupModelsDir();
|
|
1225
|
+
this.downloadManager = DownloadManager.getInstance(this.cacheDir, this.modelsDir);
|
|
1226
|
+
this.tokenizerManager = TokenizerManager.getInstance(this.cacheDir, this.modelsDir);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Sets up the models directory, reading from config or environment variables,
|
|
1230
|
+
* and ensures the directory exists.
|
|
1231
|
+
*/
|
|
1232
|
+
_setupModelsDir() {
|
|
1233
|
+
const modelsDirEnv = this.config?.MODELS_DIR?.trim() || process.env.MODELS_DIR?.trim();
|
|
1234
|
+
if (modelsDirEnv) {
|
|
1235
|
+
this.modelsDir = path2.resolve(modelsDirEnv);
|
|
1236
|
+
logger5.info("Using models directory from MODELS_DIR environment variable:", this.modelsDir);
|
|
1237
|
+
} else {
|
|
1238
|
+
this.modelsDir = path2.join(os2.homedir(), ".eliza", "models");
|
|
1239
|
+
logger5.info(
|
|
1240
|
+
"MODELS_DIR environment variable not set, using default models directory:",
|
|
1241
|
+
this.modelsDir
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
if (!fs2.existsSync(this.modelsDir)) {
|
|
1245
|
+
fs2.mkdirSync(this.modelsDir, { recursive: true });
|
|
1246
|
+
logger5.debug("Ensured models directory exists (created):", this.modelsDir);
|
|
1247
|
+
} else {
|
|
1248
|
+
logger5.debug("Models directory already exists:", this.modelsDir);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Sets up the cache directory, reading from config or environment variables,
|
|
1253
|
+
* and ensures the directory exists.
|
|
1254
|
+
*/
|
|
1255
|
+
_setupCacheDir() {
|
|
1256
|
+
const cacheDirEnv = this.config?.CACHE_DIR?.trim() || process.env.CACHE_DIR?.trim();
|
|
1257
|
+
if (cacheDirEnv) {
|
|
1258
|
+
this.cacheDir = path2.resolve(cacheDirEnv);
|
|
1259
|
+
logger5.info("Using cache directory from CACHE_DIR environment variable:", this.cacheDir);
|
|
1260
|
+
} else {
|
|
1261
|
+
const cacheDir = path2.join(os2.homedir(), ".eliza", "cache");
|
|
1262
|
+
if (!fs2.existsSync(cacheDir)) {
|
|
1263
|
+
fs2.mkdirSync(cacheDir, { recursive: true });
|
|
1264
|
+
logger5.debug("Ensuring cache directory exists (created):", cacheDir);
|
|
1265
|
+
}
|
|
1266
|
+
this.cacheDir = cacheDir;
|
|
1267
|
+
logger5.info(
|
|
1268
|
+
"CACHE_DIR environment variable not set, using default cache directory:",
|
|
1269
|
+
this.cacheDir
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
if (!fs2.existsSync(this.cacheDir)) {
|
|
1273
|
+
fs2.mkdirSync(this.cacheDir, { recursive: true });
|
|
1274
|
+
logger5.debug("Ensured cache directory exists (created):", this.cacheDir);
|
|
1275
|
+
} else {
|
|
1276
|
+
logger5.debug("Cache directory already exists:", this.cacheDir);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
/**
|
|
1280
|
+
* Retrieves the singleton instance of LocalAIManager. If an instance does not already exist, a new one is created and returned.
|
|
1281
|
+
* @returns {LocalAIManager} The singleton instance of LocalAIManager
|
|
1282
|
+
*/
|
|
1283
|
+
static getInstance() {
|
|
1284
|
+
if (!_LocalAIManager.instance) {
|
|
1285
|
+
_LocalAIManager.instance = new _LocalAIManager();
|
|
1286
|
+
}
|
|
1287
|
+
return _LocalAIManager.instance;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Initializes the environment by validating the configuration and setting model paths.
|
|
1291
|
+
* Now public to be callable from plugin init and model handlers.
|
|
1292
|
+
*
|
|
1293
|
+
* @returns {Promise<void>} A Promise that resolves once the environment has been successfully initialized.
|
|
1294
|
+
*/
|
|
1295
|
+
async initializeEnvironment() {
|
|
1296
|
+
if (this.environmentInitialized) return;
|
|
1297
|
+
if (this.environmentInitializingPromise) {
|
|
1298
|
+
await this.environmentInitializingPromise;
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
this.environmentInitializingPromise = (async () => {
|
|
1302
|
+
try {
|
|
1303
|
+
logger5.info("Initializing environment configuration...");
|
|
1304
|
+
this.config = await validateConfig();
|
|
1305
|
+
this.embeddingModelConfig = resolveEmbeddingModelSpec(this.config, MODEL_SPECS.embedding);
|
|
1306
|
+
this._postValidateInit();
|
|
1307
|
+
this.embeddingModelPath = path2.join(this.modelsDir, this.embeddingModelConfig.name);
|
|
1308
|
+
logger5.info("Using embedding model path:", basename(this.embeddingModelPath));
|
|
1309
|
+
logger5.info(
|
|
1310
|
+
{
|
|
1311
|
+
model: this.embeddingModelConfig.name,
|
|
1312
|
+
repo: this.embeddingModelConfig.repo,
|
|
1313
|
+
dimensions: this.embeddingModelConfig.dimensions,
|
|
1314
|
+
contextSize: this.embeddingModelConfig.contextSize
|
|
1315
|
+
},
|
|
1316
|
+
"Resolved embedding model spec"
|
|
1317
|
+
);
|
|
1318
|
+
this.ensureEmbeddingModelFileIsValid();
|
|
1319
|
+
logger5.info("Environment configuration validated and model paths set");
|
|
1320
|
+
this.environmentInitialized = true;
|
|
1321
|
+
logger5.success("Environment initialization complete");
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
logger5.error(
|
|
1324
|
+
{
|
|
1325
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1326
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
1327
|
+
},
|
|
1328
|
+
"Environment validation failed"
|
|
1329
|
+
);
|
|
1330
|
+
this.environmentInitializingPromise = null;
|
|
1331
|
+
throw error;
|
|
1332
|
+
}
|
|
1333
|
+
})();
|
|
1334
|
+
await this.environmentInitializingPromise;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Downloads the model based on the modelPath provided.
|
|
1338
|
+
* Determines the model spec and path based on the model type.
|
|
1339
|
+
*
|
|
1340
|
+
* @param {ModelTypeName} modelType - The type of model to download
|
|
1341
|
+
* @param {ModelSpec} [customModelSpec] - Optional custom model spec to use instead of the default
|
|
1342
|
+
* @returns A Promise that resolves to a boolean indicating whether the model download was successful.
|
|
1343
|
+
*/
|
|
1344
|
+
async downloadModel(modelType, customModelSpec, forceDownload = false) {
|
|
1345
|
+
let modelSpec;
|
|
1346
|
+
let modelPathToDownload;
|
|
1347
|
+
await this.initializeEnvironment();
|
|
1348
|
+
if (customModelSpec) {
|
|
1349
|
+
modelSpec = customModelSpec;
|
|
1350
|
+
modelPathToDownload = modelType === ModelType.TEXT_EMBEDDING ? this.embeddingModelPath : modelType === ModelType.TEXT_LARGE ? this.mediumModelPath : this.modelPath;
|
|
1351
|
+
} else if (modelType === ModelType.TEXT_EMBEDDING) {
|
|
1352
|
+
modelSpec = this.embeddingModelConfig;
|
|
1353
|
+
modelPathToDownload = this.embeddingModelPath;
|
|
1354
|
+
this.ensureEmbeddingModelFileIsValid();
|
|
1355
|
+
} else {
|
|
1356
|
+
modelSpec = modelType === ModelType.TEXT_LARGE ? MODEL_SPECS.medium : MODEL_SPECS.small;
|
|
1357
|
+
modelPathToDownload = modelType === ModelType.TEXT_LARGE ? this.mediumModelPath : this.modelPath;
|
|
1358
|
+
}
|
|
1359
|
+
try {
|
|
1360
|
+
return await this.downloadManager.downloadModel(
|
|
1361
|
+
modelSpec,
|
|
1362
|
+
modelPathToDownload,
|
|
1363
|
+
forceDownload
|
|
1364
|
+
);
|
|
1365
|
+
} catch (error) {
|
|
1366
|
+
logger5.error(
|
|
1367
|
+
{
|
|
1368
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1369
|
+
modelType,
|
|
1370
|
+
modelPath: modelPathToDownload
|
|
1371
|
+
},
|
|
1372
|
+
"Model download failed"
|
|
1373
|
+
);
|
|
1374
|
+
throw error;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Asynchronously checks the platform capabilities.
|
|
1379
|
+
*
|
|
1380
|
+
* @returns {Promise<void>} A promise that resolves once the platform capabilities have been checked.
|
|
1381
|
+
*/
|
|
1382
|
+
async checkPlatformCapabilities() {
|
|
1383
|
+
try {
|
|
1384
|
+
const platformManager = getPlatformManager();
|
|
1385
|
+
await platformManager.initialize();
|
|
1386
|
+
const capabilities = platformManager.getCapabilities();
|
|
1387
|
+
logger5.info(
|
|
1388
|
+
{
|
|
1389
|
+
platform: capabilities.platform,
|
|
1390
|
+
gpu: capabilities.gpu?.type || "none",
|
|
1391
|
+
recommendedModel: capabilities.recommendedModelSize,
|
|
1392
|
+
supportedBackends: capabilities.supportedBackends
|
|
1393
|
+
},
|
|
1394
|
+
"Platform capabilities detected"
|
|
1395
|
+
);
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
logger5.warn(error instanceof Error ? error : String(error), "Platform detection failed");
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Initializes the LocalAI Manager for a given model type.
|
|
1402
|
+
*
|
|
1403
|
+
* @param {ModelTypeName} modelType - The type of model to initialize (default: ModelType.TEXT_SMALL)
|
|
1404
|
+
* @returns {Promise<void>} A promise that resolves when initialization is complete or rejects if an error occurs
|
|
1405
|
+
*/
|
|
1406
|
+
async initialize(_modelType = ModelType.TEXT_SMALL) {
|
|
1407
|
+
await this.initializeEnvironment();
|
|
1408
|
+
}
|
|
1409
|
+
getEmbeddingDimensions() {
|
|
1410
|
+
return this.embeddingModelConfig.dimensions;
|
|
1411
|
+
}
|
|
1412
|
+
ensureEmbeddingModelFileIsValid() {
|
|
1413
|
+
if (!this.embeddingModelPath || !fs2.existsSync(this.embeddingModelPath)) return;
|
|
1414
|
+
if (isValidGgufFile(this.embeddingModelPath)) return;
|
|
1415
|
+
const { bytesRead, magic } = readMagicHeader(this.embeddingModelPath);
|
|
1416
|
+
logger5.warn(
|
|
1417
|
+
{
|
|
1418
|
+
embeddingModelPath: this.embeddingModelPath,
|
|
1419
|
+
bytesRead,
|
|
1420
|
+
magic
|
|
1421
|
+
},
|
|
1422
|
+
"Invalid embedding model file detected; removing corrupt file before download/retry"
|
|
1423
|
+
);
|
|
1424
|
+
safeUnlink(this.embeddingModelPath);
|
|
1425
|
+
}
|
|
1426
|
+
async ensureLlama() {
|
|
1427
|
+
if (this.llama) return;
|
|
1428
|
+
this.llama = await getLlama({
|
|
1429
|
+
logLevel: LlamaLogLevel.error,
|
|
1430
|
+
logger: (level, message) => {
|
|
1431
|
+
if (level !== "error" && level !== "fatal") return;
|
|
1432
|
+
const text = message.trim();
|
|
1433
|
+
if (!text) return;
|
|
1434
|
+
if (shouldSuppressNodeLlamaLoadError(text)) return;
|
|
1435
|
+
logger5.error(`[node-llama-cpp] ${text}`);
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
async loadEmbeddingModel() {
|
|
1440
|
+
this.ensureEmbeddingModelFileIsValid();
|
|
1441
|
+
const gpuLayers = this.config?.LOCAL_EMBEDDING_GPU_LAYERS === -1 ? "auto" : this.config?.LOCAL_EMBEDDING_GPU_LAYERS ?? 0;
|
|
1442
|
+
const useMmap = this.config?.LOCAL_EMBEDDING_USE_MMAP ?? true;
|
|
1443
|
+
this.embeddingModel = await this.llama.loadModel({
|
|
1444
|
+
modelPath: this.embeddingModelPath,
|
|
1445
|
+
gpuLayers,
|
|
1446
|
+
vocabOnly: false,
|
|
1447
|
+
useMmap
|
|
1448
|
+
});
|
|
1449
|
+
this.embeddingContext = await this.embeddingModel.createEmbeddingContext({
|
|
1450
|
+
contextSize: this.embeddingModelConfig.contextSize,
|
|
1451
|
+
batchSize: 512
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
async initializeEmbeddingWithRecovery() {
|
|
1455
|
+
logger5.info("Loading embedding model:", this.embeddingModelPath);
|
|
1456
|
+
try {
|
|
1457
|
+
await this.loadEmbeddingModel();
|
|
1458
|
+
logger5.success("Embedding model initialized successfully");
|
|
1459
|
+
return;
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
if (!isCorruptedModelLoadError(error)) {
|
|
1462
|
+
throw error;
|
|
1463
|
+
}
|
|
1464
|
+
logger5.warn(
|
|
1465
|
+
{
|
|
1466
|
+
error: getErrorMessage(error),
|
|
1467
|
+
embeddingModelPath: this.embeddingModelPath
|
|
1468
|
+
},
|
|
1469
|
+
"Embedding model appears corrupted/incomplete; deleting and re-downloading"
|
|
1470
|
+
);
|
|
1471
|
+
this.embeddingModel = void 0;
|
|
1472
|
+
this.embeddingContext = void 0;
|
|
1473
|
+
safeUnlink(this.embeddingModelPath);
|
|
1474
|
+
await this.downloadModel(ModelType.TEXT_EMBEDDING, void 0, true);
|
|
1475
|
+
this.ensureEmbeddingModelFileIsValid();
|
|
1476
|
+
await this.loadEmbeddingModel();
|
|
1477
|
+
logger5.success("Embedding model recovered after re-download");
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Asynchronously initializes the embedding model.
|
|
1482
|
+
*
|
|
1483
|
+
* @returns {Promise<void>} A promise that resolves once the initialization is complete.
|
|
1484
|
+
*/
|
|
1485
|
+
async initializeEmbedding() {
|
|
1486
|
+
try {
|
|
1487
|
+
await this.initializeEnvironment();
|
|
1488
|
+
logger5.info("Initializing embedding model...");
|
|
1489
|
+
logger5.info("Models directory:", this.modelsDir);
|
|
1490
|
+
if (!fs2.existsSync(this.modelsDir)) {
|
|
1491
|
+
logger5.warn("Models directory does not exist, creating it:", this.modelsDir);
|
|
1492
|
+
fs2.mkdirSync(this.modelsDir, { recursive: true });
|
|
1493
|
+
}
|
|
1494
|
+
await this.downloadModel(ModelType.TEXT_EMBEDDING);
|
|
1495
|
+
this.ensureEmbeddingModelFileIsValid();
|
|
1496
|
+
await this.ensureLlama();
|
|
1497
|
+
if (!this.embeddingModel) {
|
|
1498
|
+
await this.initializeEmbeddingWithRecovery();
|
|
1499
|
+
}
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
if (isCorruptedModelLoadError(error)) {
|
|
1502
|
+
logger5.warn(
|
|
1503
|
+
{
|
|
1504
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1505
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1506
|
+
modelsDir: this.modelsDir,
|
|
1507
|
+
embeddingModelPath: this.embeddingModelPath
|
|
1508
|
+
},
|
|
1509
|
+
"Embedding initialization failed due to model corruption"
|
|
1510
|
+
);
|
|
1511
|
+
safeUnlink(this.embeddingModelPath);
|
|
1512
|
+
} else {
|
|
1513
|
+
logger5.error(
|
|
1514
|
+
{
|
|
1515
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1516
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1517
|
+
modelsDir: this.modelsDir,
|
|
1518
|
+
embeddingModelPath: this.embeddingModelPath
|
|
1519
|
+
// Log the path being used
|
|
1520
|
+
},
|
|
1521
|
+
"Embedding initialization failed with details"
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
throw error;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Generate embeddings using the proper LlamaContext.getEmbedding method.
|
|
1529
|
+
*/
|
|
1530
|
+
async generateEmbedding(text) {
|
|
1531
|
+
try {
|
|
1532
|
+
await this.lazyInitEmbedding();
|
|
1533
|
+
if (!this.embeddingModel || !this.embeddingContext) {
|
|
1534
|
+
throw new Error("Failed to initialize embedding model");
|
|
1535
|
+
}
|
|
1536
|
+
logger5.info({ textLength: text.length }, "Generating embedding for text");
|
|
1537
|
+
let candidateText = text;
|
|
1538
|
+
let attempt = 0;
|
|
1539
|
+
while (true) {
|
|
1540
|
+
try {
|
|
1541
|
+
const embeddingResult = await this.embeddingContext.getEmbeddingFor(candidateText);
|
|
1542
|
+
const mutableEmbedding = [...embeddingResult.vector];
|
|
1543
|
+
const sizedEmbedding = this.alignEmbeddingDimensions(mutableEmbedding);
|
|
1544
|
+
const normalizedEmbedding = this.normalizeEmbedding(sizedEmbedding);
|
|
1545
|
+
logger5.info({ dimensions: normalizedEmbedding.length }, "Embedding generation complete");
|
|
1546
|
+
return normalizedEmbedding;
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
if (!isContextLimitError(error)) {
|
|
1549
|
+
throw error;
|
|
1550
|
+
}
|
|
1551
|
+
const nextCandidate = shrinkEmbeddingInput(candidateText);
|
|
1552
|
+
if (nextCandidate === candidateText) {
|
|
1553
|
+
throw error;
|
|
1554
|
+
}
|
|
1555
|
+
attempt += 1;
|
|
1556
|
+
logger5.warn(
|
|
1557
|
+
{
|
|
1558
|
+
attempt,
|
|
1559
|
+
currentChars: candidateText.length,
|
|
1560
|
+
nextChars: nextCandidate.length
|
|
1561
|
+
},
|
|
1562
|
+
"Embedding input exceeded context window; retrying with truncated text"
|
|
1563
|
+
);
|
|
1564
|
+
candidateText = nextCandidate;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
if (isCorruptedModelLoadError(error)) {
|
|
1569
|
+
logger5.warn(
|
|
1570
|
+
{
|
|
1571
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1572
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1573
|
+
textLength: text?.length ?? "text is null",
|
|
1574
|
+
embeddingModelPath: this.embeddingModelPath
|
|
1575
|
+
},
|
|
1576
|
+
"Embedding generation failed due to model corruption; model file removed"
|
|
1577
|
+
);
|
|
1578
|
+
safeUnlink(this.embeddingModelPath);
|
|
1579
|
+
this.embeddingModel = void 0;
|
|
1580
|
+
this.embeddingContext = void 0;
|
|
1581
|
+
this.embeddingInitialized = false;
|
|
1582
|
+
} else {
|
|
1583
|
+
logger5.error(
|
|
1584
|
+
{
|
|
1585
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1586
|
+
stack: error instanceof Error ? error.stack : void 0,
|
|
1587
|
+
textLength: text?.length ?? "text is null"
|
|
1588
|
+
},
|
|
1589
|
+
"Embedding generation failed"
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
const zeroDimensions = this.getEmbeddingDimensions();
|
|
1593
|
+
return new Array(zeroDimensions).fill(0);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
/**
|
|
1597
|
+
* Normalizes an embedding vector using L2 normalization
|
|
1598
|
+
*
|
|
1599
|
+
* @param {number[]} embedding - The embedding vector to normalize
|
|
1600
|
+
* @returns {number[]} - The normalized embedding vector
|
|
1601
|
+
*/
|
|
1602
|
+
alignEmbeddingDimensions(embedding) {
|
|
1603
|
+
const targetDimensions = this.getEmbeddingDimensions();
|
|
1604
|
+
if (targetDimensions <= 0 || embedding.length === targetDimensions) {
|
|
1605
|
+
return embedding;
|
|
1606
|
+
}
|
|
1607
|
+
logger5.warn(
|
|
1608
|
+
{
|
|
1609
|
+
observedDimensions: embedding.length,
|
|
1610
|
+
targetDimensions
|
|
1611
|
+
},
|
|
1612
|
+
"Embedding dimensions mismatch; adjusting output dimensions"
|
|
1613
|
+
);
|
|
1614
|
+
if (embedding.length > targetDimensions) {
|
|
1615
|
+
return embedding.slice(0, targetDimensions);
|
|
1616
|
+
}
|
|
1617
|
+
return [...embedding, ...new Array(targetDimensions - embedding.length).fill(0)];
|
|
1618
|
+
}
|
|
1619
|
+
normalizeEmbedding(embedding) {
|
|
1620
|
+
const squareSum = embedding.reduce((sum, val) => sum + val * val, 0);
|
|
1621
|
+
const norm = Math.sqrt(squareSum);
|
|
1622
|
+
if (norm === 0) {
|
|
1623
|
+
return embedding;
|
|
1624
|
+
}
|
|
1625
|
+
return embedding.map((val) => val / norm);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Lazy initialize the embedding model
|
|
1629
|
+
*/
|
|
1630
|
+
async lazyInitEmbedding() {
|
|
1631
|
+
if (this.embeddingInitialized) return;
|
|
1632
|
+
if (!this.embeddingInitializingPromise) {
|
|
1633
|
+
this.embeddingInitializingPromise = (async () => {
|
|
1634
|
+
try {
|
|
1635
|
+
await this.initializeEnvironment();
|
|
1636
|
+
await this.downloadModel(ModelType.TEXT_EMBEDDING);
|
|
1637
|
+
this.ensureEmbeddingModelFileIsValid();
|
|
1638
|
+
await this.ensureLlama();
|
|
1639
|
+
await this.initializeEmbeddingWithRecovery();
|
|
1640
|
+
this.embeddingInitialized = true;
|
|
1641
|
+
logger5.info("Embedding model initialized successfully");
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
if (isCorruptedModelLoadError(error)) {
|
|
1644
|
+
logger5.warn(
|
|
1645
|
+
error instanceof Error ? error : String(error),
|
|
1646
|
+
"Failed to initialize embedding model due to corruption"
|
|
1647
|
+
);
|
|
1648
|
+
safeUnlink(this.embeddingModelPath);
|
|
1649
|
+
} else {
|
|
1650
|
+
logger5.error(
|
|
1651
|
+
error instanceof Error ? error : String(error),
|
|
1652
|
+
"Failed to initialize embedding model"
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
this.embeddingInitializingPromise = null;
|
|
1656
|
+
throw error;
|
|
1657
|
+
}
|
|
1658
|
+
})();
|
|
1659
|
+
}
|
|
1660
|
+
await this.embeddingInitializingPromise;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Returns the TokenizerManager associated with this object.
|
|
1664
|
+
*
|
|
1665
|
+
* @returns {TokenizerManager} The TokenizerManager object.
|
|
1666
|
+
*/
|
|
1667
|
+
getTokenizerManager() {
|
|
1668
|
+
return this.tokenizerManager;
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Returns the active model configuration.
|
|
1672
|
+
* @returns {ModelSpec} The active model configuration.
|
|
1673
|
+
*/
|
|
1674
|
+
getActiveModelConfig() {
|
|
1675
|
+
return this.activeModelConfig;
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
var localAIManager = LocalAIManager.getInstance();
|
|
1679
|
+
var localAiPlugin = {
|
|
1680
|
+
name: "local-ai",
|
|
1681
|
+
description: "Local AI plugin using LLaMA models",
|
|
1682
|
+
// Higher priority ensures local embeddings are used instead of remote
|
|
1683
|
+
// providers (e.g. ElizaCloud, OpenAI) even when plugins register in
|
|
1684
|
+
// parallel and the registration order is non-deterministic.
|
|
1685
|
+
priority: 10,
|
|
1686
|
+
async init(_config, _runtime) {
|
|
1687
|
+
logger5.info("Initializing local embedding plugin...");
|
|
1688
|
+
try {
|
|
1689
|
+
await localAIManager.initializeEnvironment();
|
|
1690
|
+
await localAIManager.checkPlatformCapabilities();
|
|
1691
|
+
const config = validateConfig();
|
|
1692
|
+
const modelsDir = config.MODELS_DIR || path2.join(os2.homedir(), ".eliza", "models");
|
|
1693
|
+
const embeddingModelPath = path2.join(modelsDir, config.LOCAL_EMBEDDING_MODEL);
|
|
1694
|
+
if (fs2.existsSync(embeddingModelPath)) {
|
|
1695
|
+
logger5.info(
|
|
1696
|
+
{ embeddingModelPath: basename(embeddingModelPath) },
|
|
1697
|
+
"Embedding model file is present"
|
|
1698
|
+
);
|
|
1699
|
+
} else {
|
|
1700
|
+
logger5.info(
|
|
1701
|
+
{ embeddingModelPath: basename(embeddingModelPath) },
|
|
1702
|
+
"Embedding model file not present yet; it will be downloaded on first use"
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
logger5.success("Local embedding plugin initialized");
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
logger5.error(
|
|
1708
|
+
{
|
|
1709
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1710
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
1711
|
+
},
|
|
1712
|
+
"Failed to initialize local embedding plugin"
|
|
1713
|
+
);
|
|
1714
|
+
logger5.warn("Local embedding plugin may be unavailable");
|
|
1715
|
+
}
|
|
1716
|
+
},
|
|
1717
|
+
models: {
|
|
1718
|
+
[ModelType.TEXT_EMBEDDING]: async (_runtime, params) => {
|
|
1719
|
+
let text;
|
|
1720
|
+
if (typeof params === "string") {
|
|
1721
|
+
text = params;
|
|
1722
|
+
} else if (params && typeof params === "object" && "text" in params) {
|
|
1723
|
+
text = params.text;
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
if (!text) {
|
|
1727
|
+
logger5.debug("Null or empty text input for embedding, returning zero vector");
|
|
1728
|
+
return new Array(localAIManager.getEmbeddingDimensions()).fill(0);
|
|
1729
|
+
}
|
|
1730
|
+
return await localAIManager.generateEmbedding(text);
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
logger5.error(
|
|
1733
|
+
{
|
|
1734
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1735
|
+
fullText: text,
|
|
1736
|
+
textType: typeof text,
|
|
1737
|
+
textStructure: text !== null ? JSON.stringify(text, null, 2) : "null"
|
|
1738
|
+
},
|
|
1739
|
+
"Error in TEXT_EMBEDDING handler"
|
|
1740
|
+
);
|
|
1741
|
+
return new Array(localAIManager.getEmbeddingDimensions()).fill(0);
|
|
1742
|
+
}
|
|
1743
|
+
},
|
|
1744
|
+
[ModelType.TEXT_TOKENIZER_ENCODE]: async (_runtime, params) => {
|
|
1745
|
+
try {
|
|
1746
|
+
const manager = localAIManager.getTokenizerManager();
|
|
1747
|
+
const config = localAIManager.getActiveModelConfig();
|
|
1748
|
+
return await manager.encode(params.prompt, config);
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
logger5.error(
|
|
1751
|
+
error instanceof Error ? error : String(error),
|
|
1752
|
+
"Error in TEXT_TOKENIZER_ENCODE handler"
|
|
1753
|
+
);
|
|
1754
|
+
throw error;
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1757
|
+
[ModelType.TEXT_TOKENIZER_DECODE]: async (_runtime, params) => {
|
|
1758
|
+
try {
|
|
1759
|
+
const manager = localAIManager.getTokenizerManager();
|
|
1760
|
+
const config = localAIManager.getActiveModelConfig();
|
|
1761
|
+
return await manager.decode(params.tokens, config);
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
logger5.error(
|
|
1764
|
+
error instanceof Error ? error : String(error),
|
|
1765
|
+
"Error in TEXT_TOKENIZER_DECODE handler"
|
|
1766
|
+
);
|
|
1767
|
+
throw error;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
},
|
|
1771
|
+
tests: [
|
|
1772
|
+
{
|
|
1773
|
+
name: "local_ai_plugin_tests",
|
|
1774
|
+
tests: [
|
|
1775
|
+
{
|
|
1776
|
+
name: "local_ai_test_text_embedding",
|
|
1777
|
+
fn: async (runtime) => {
|
|
1778
|
+
try {
|
|
1779
|
+
logger5.info("Starting TEXT_EMBEDDING test");
|
|
1780
|
+
const embedding = await runtime.useModel(ModelType.TEXT_EMBEDDING, {
|
|
1781
|
+
text: "This is a test of the text embedding model."
|
|
1782
|
+
});
|
|
1783
|
+
logger5.info({ count: embedding.length }, "Embedding generated with dimensions");
|
|
1784
|
+
if (!Array.isArray(embedding)) {
|
|
1785
|
+
throw new Error("Embedding is not an array");
|
|
1786
|
+
}
|
|
1787
|
+
if (embedding.length === 0) {
|
|
1788
|
+
throw new Error("Embedding array is empty");
|
|
1789
|
+
}
|
|
1790
|
+
if (embedding.some((val) => typeof val !== "number")) {
|
|
1791
|
+
throw new Error("Embedding contains non-numeric values");
|
|
1792
|
+
}
|
|
1793
|
+
const nullEmbedding = await runtime.useModel(ModelType.TEXT_EMBEDDING, null);
|
|
1794
|
+
if (!Array.isArray(nullEmbedding) || nullEmbedding.some((val) => val !== 0)) {
|
|
1795
|
+
throw new Error("Null input did not return zero vector");
|
|
1796
|
+
}
|
|
1797
|
+
logger5.success("TEXT_EMBEDDING test completed successfully");
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
logger5.error(
|
|
1800
|
+
{
|
|
1801
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1802
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
1803
|
+
},
|
|
1804
|
+
"TEXT_EMBEDDING test failed"
|
|
1805
|
+
);
|
|
1806
|
+
throw error;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
name: "local_ai_test_tokenizer_encode",
|
|
1812
|
+
fn: async (runtime) => {
|
|
1813
|
+
try {
|
|
1814
|
+
logger5.info("Starting TEXT_TOKENIZER_ENCODE test");
|
|
1815
|
+
const prompt = "Hello tokenizer test!";
|
|
1816
|
+
const tokens = await runtime.useModel(ModelType.TEXT_TOKENIZER_ENCODE, {
|
|
1817
|
+
prompt,
|
|
1818
|
+
modelType: ModelType.TEXT_TOKENIZER_ENCODE
|
|
1819
|
+
});
|
|
1820
|
+
logger5.info({ count: tokens.length }, "Encoded tokens");
|
|
1821
|
+
if (!Array.isArray(tokens)) {
|
|
1822
|
+
throw new Error("Tokens output is not an array");
|
|
1823
|
+
}
|
|
1824
|
+
if (tokens.length === 0) {
|
|
1825
|
+
throw new Error("No tokens generated");
|
|
1826
|
+
}
|
|
1827
|
+
if (tokens.some((token) => !Number.isInteger(token))) {
|
|
1828
|
+
throw new Error("Tokens contain non-integer values");
|
|
1829
|
+
}
|
|
1830
|
+
logger5.success("TEXT_TOKENIZER_ENCODE test completed successfully");
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
logger5.error(
|
|
1833
|
+
{
|
|
1834
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1835
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
1836
|
+
},
|
|
1837
|
+
"TEXT_TOKENIZER_ENCODE test failed"
|
|
1838
|
+
);
|
|
1839
|
+
throw error;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
},
|
|
1843
|
+
{
|
|
1844
|
+
name: "local_ai_test_tokenizer_decode",
|
|
1845
|
+
fn: async (runtime) => {
|
|
1846
|
+
try {
|
|
1847
|
+
logger5.info("Starting TEXT_TOKENIZER_DECODE test");
|
|
1848
|
+
const originalText = "Hello tokenizer test!";
|
|
1849
|
+
const tokens = await runtime.useModel(ModelType.TEXT_TOKENIZER_ENCODE, {
|
|
1850
|
+
prompt: originalText,
|
|
1851
|
+
modelType: ModelType.TEXT_TOKENIZER_ENCODE
|
|
1852
|
+
});
|
|
1853
|
+
const decodedText = await runtime.useModel(ModelType.TEXT_TOKENIZER_DECODE, {
|
|
1854
|
+
tokens,
|
|
1855
|
+
modelType: ModelType.TEXT_TOKENIZER_DECODE
|
|
1856
|
+
});
|
|
1857
|
+
logger5.info(
|
|
1858
|
+
{ original: originalText, decoded: decodedText },
|
|
1859
|
+
"Round trip tokenization"
|
|
1860
|
+
);
|
|
1861
|
+
if (typeof decodedText !== "string") {
|
|
1862
|
+
throw new Error("Decoded output is not a string");
|
|
1863
|
+
}
|
|
1864
|
+
logger5.success("TEXT_TOKENIZER_DECODE test completed successfully");
|
|
1865
|
+
} catch (error) {
|
|
1866
|
+
logger5.error(
|
|
1867
|
+
{
|
|
1868
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1869
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
1870
|
+
},
|
|
1871
|
+
"TEXT_TOKENIZER_DECODE test failed"
|
|
1872
|
+
);
|
|
1873
|
+
throw error;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
]
|
|
1878
|
+
}
|
|
1879
|
+
]
|
|
1880
|
+
};
|
|
1881
|
+
var index_default = localAiPlugin;
|
|
1882
|
+
export {
|
|
1883
|
+
index_default as default,
|
|
1884
|
+
localAiPlugin
|
|
1885
|
+
};
|
|
1886
|
+
//# sourceMappingURL=index.js.map
|