@elizaos/plugin-local-embedding 2.0.0-alpha.3

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