@@ -33,6 +33,13 @@ import path from "path";
33
33
import { fileURLToPath } from "url" ;
34
34
import { character } from "./character.ts" ;
35
35
import type { DirectClient } from "@ai16z/client-direct" ;
36
+ import { Pool } from "pg" ;
37
+ import { EncryptionUtil } from "@ai16z/eliza" ;
38
+ import express , { Request as ExpressRequest } from "express" ;
39
+ import bodyParser from "body-parser" ;
40
+ import cors from "cors" ;
41
+
42
+ let globalDirectClient : DirectClient | null = null ;
36
43
37
44
const __filename = fileURLToPath ( import . meta. url ) ; // get the resolved path to the file
38
45
const __dirname = path . dirname ( __filename ) ; // get the name of the directory
@@ -306,8 +313,17 @@ async function startAgent(character: Character, directClient: DirectClient) {
306
313
}
307
314
}
308
315
316
+ export function setGlobalDirectClient ( client : DirectClient ) {
317
+ globalDirectClient = client ;
318
+ }
319
+
320
+ export function getGlobalDirectClient ( ) : DirectClient | null {
321
+ return globalDirectClient ;
322
+ }
323
+
309
324
const startAgents = async ( ) => {
310
325
const directClient = await DirectClientInterface . start ( ) ;
326
+ setGlobalDirectClient ( directClient as DirectClient ) ;
311
327
const args = parseArguments ( ) ;
312
328
313
329
let charactersArg = args . characters || args . character ;
@@ -318,6 +334,15 @@ const startAgents = async () => {
318
334
characters = await loadCharacters ( charactersArg ) ;
319
335
}
320
336
337
+ const shouldFetchFromDb = process . env . FETCH_FROM_DB === "true" ;
338
+
339
+ if ( shouldFetchFromDb ) {
340
+ characters = await loadCharactersFromDb ( charactersArg ) ;
341
+ if ( characters . length === 0 ) {
342
+ characters = [ character ] ;
343
+ }
344
+ }
345
+
321
346
try {
322
347
for ( const character of characters ) {
323
348
await startAgent ( character , directClient as DirectClient ) ;
@@ -384,3 +409,174 @@ async function handleUserInput(input, agentId) {
384
409
console . error ( "Error fetching response:" , error ) ;
385
410
}
386
411
}
412
+
413
+ /**
414
+ * Loads characters from PostgreSQL database
415
+ * @param characterNames - Optional comma-separated list of character names to load
416
+ * @returns Promise of loaded and decrypted characters
417
+ */
418
+ export async function loadCharactersFromDb (
419
+ characterNames ?: string
420
+ ) : Promise < Character [ ] > {
421
+ try {
422
+ const encryptionUtil = new EncryptionUtil (
423
+ process . env . ENCRYPTION_KEY || "default-key"
424
+ ) ;
425
+
426
+ const dataDir = path . join ( __dirname , "../data" ) ;
427
+
428
+ if ( ! fs . existsSync ( dataDir ) ) {
429
+ fs . mkdirSync ( dataDir , { recursive : true } ) ;
430
+ }
431
+
432
+ const db = initializeDatabase ( dataDir ) ;
433
+ await db . init ( ) ;
434
+
435
+ // Convert names to UUIDs if provided
436
+ const characterIds = characterNames
437
+ ?. split ( "," )
438
+ . map ( ( name ) => name . trim ( ) )
439
+ . map ( ( name ) => stringToUuid ( name ) ) ;
440
+
441
+ // Get characters and their secretsIVs from database
442
+ const [ characters , secretsIVs ] = await db . loadCharacters ( characterIds ) ;
443
+
444
+ if ( characters . length === 0 ) {
445
+ elizaLogger . log (
446
+ "No characters found in database, using default character"
447
+ ) ;
448
+ return [ ] ;
449
+ }
450
+
451
+ // Process each character with its corresponding secretsIV
452
+ const processedCharacters = await Promise . all (
453
+ characters . map ( async ( character , index ) => {
454
+ try {
455
+ // Decrypt secrets if they exist
456
+ if ( character . settings ?. secrets ) {
457
+ const decryptedSecrets : { [ key : string ] : string } = { } ;
458
+ const secretsIV = secretsIVs [ index ] ;
459
+
460
+ for ( const [ key , encryptedValue ] of Object . entries (
461
+ character . settings . secrets
462
+ ) ) {
463
+ const iv = secretsIV [ key ] ;
464
+ if ( ! iv ) {
465
+ elizaLogger . error (
466
+ `Missing IV for secret ${ key } in character ${ character . name } `
467
+ ) ;
468
+ continue ;
469
+ }
470
+
471
+ try {
472
+ decryptedSecrets [ key ] = encryptionUtil . decrypt ( {
473
+ encryptedText : encryptedValue ,
474
+ iv,
475
+ } ) ;
476
+ } catch ( error ) {
477
+ elizaLogger . error (
478
+ `Failed to decrypt secret ${ key } for character ${ character . name } :` ,
479
+ error
480
+ ) ;
481
+ }
482
+ }
483
+ character . settings . secrets = decryptedSecrets ;
484
+ }
485
+
486
+ // Handle plugins
487
+ if ( character . plugins ) {
488
+ elizaLogger . log ( "Plugins are: " , character . plugins ) ;
489
+ const importedPlugins = await Promise . all (
490
+ character . plugins . map ( async ( plugin ) => {
491
+ // if the plugin name doesnt start with @eliza ,
492
+
493
+ const importedPlugin = await import (
494
+ plugin . name
495
+ ) ;
496
+ return importedPlugin ;
497
+ } )
498
+ ) ;
499
+
500
+ character . plugins = importedPlugins ;
501
+ }
502
+
503
+ validateCharacterConfig ( character ) ;
504
+ elizaLogger . log (
505
+ `Character loaded from db: ${ character . name } `
506
+ ) ;
507
+ console . log ( "-------------------------------" ) ;
508
+ return character ;
509
+ } catch ( error ) {
510
+ elizaLogger . error (
511
+ `Error processing character ${ character . name } :` ,
512
+ error
513
+ ) ;
514
+ throw error ;
515
+ }
516
+ } )
517
+ ) ;
518
+
519
+ return processedCharacters ;
520
+ } catch ( error ) {
521
+ elizaLogger . error ( "Database error:" , error ) ;
522
+ elizaLogger . log ( "Falling back to default character" ) ;
523
+ return [ defaultCharacter ] ;
524
+ }
525
+ }
526
+
527
+ // If dynamic loading is enabled, start the express server
528
+ // we can directly call this endpoint to load an agent
529
+ // otherwise we can use the direct client as a proxy if we
530
+ // want to expose only single post to public
531
+ if ( process . env . AGENT_RUNTIME_MANAGEMENT === "true" ) {
532
+ const app = express ( ) ;
533
+ app . use ( cors ( ) ) ;
534
+ app . use ( bodyParser . json ( ) ) ;
535
+
536
+ // This endpoint can be directly called or
537
+ app . post (
538
+ "/load/:agentName" ,
539
+ async ( req : ExpressRequest , res : express . Response ) => {
540
+ try {
541
+ const agentName = req . params . agentName ;
542
+ const characters = await loadCharactersFromDb ( agentName ) ;
543
+
544
+ if ( characters . length === 0 ) {
545
+ res . status ( 404 ) . json ( {
546
+ success : false ,
547
+ error : `Character ${ agentName } does not exist in DB` ,
548
+ } ) ;
549
+ return ;
550
+ }
551
+
552
+ const directClient = getGlobalDirectClient ( ) ;
553
+ await startAgent ( characters [ 0 ] , directClient ) ;
554
+
555
+ res . json ( {
556
+ success : true ,
557
+ port : settings . SERVER_PORT ,
558
+ character : {
559
+ id : characters [ 0 ] . id ,
560
+ name : characters [ 0 ] . name ,
561
+ } ,
562
+ } ) ;
563
+ } catch ( error ) {
564
+ elizaLogger . error ( `Error loading agent:` , error ) ;
565
+ res . status ( 500 ) . json ( {
566
+ success : false ,
567
+ error : error . message ,
568
+ } ) ;
569
+ }
570
+ }
571
+ ) ;
572
+
573
+ const agentPort = settings . AGENT_PORT
574
+ ? parseInt ( settings . AGENT_PORT )
575
+ : 3001 ;
576
+ //if agent port is 0, it means we want to use a random port
577
+ const server = app . listen ( agentPort , ( ) => {
578
+ elizaLogger . success (
579
+ `Agent server running at http://localhost:${ agentPort } /`
580
+ ) ;
581
+ } ) ;
582
+ }
0 commit comments