Skip to content

Commit 277ed4b

Browse files
committed
feat: loading characters from db at load and runtime
1 parent 2f06f15 commit 277ed4b

File tree

16 files changed

+1270
-18
lines changed

16 files changed

+1270
-18
lines changed

agent/src/index.ts

+196
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ import path from "path";
3333
import { fileURLToPath } from "url";
3434
import { character } from "./character.ts";
3535
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;
3643

3744
const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
3845
const __dirname = path.dirname(__filename); // get the name of the directory
@@ -306,8 +313,17 @@ async function startAgent(character: Character, directClient: DirectClient) {
306313
}
307314
}
308315

316+
export function setGlobalDirectClient(client: DirectClient) {
317+
globalDirectClient = client;
318+
}
319+
320+
export function getGlobalDirectClient(): DirectClient | null {
321+
return globalDirectClient;
322+
}
323+
309324
const startAgents = async () => {
310325
const directClient = await DirectClientInterface.start();
326+
setGlobalDirectClient(directClient as DirectClient);
311327
const args = parseArguments();
312328

313329
let charactersArg = args.characters || args.character;
@@ -318,6 +334,15 @@ const startAgents = async () => {
318334
characters = await loadCharacters(charactersArg);
319335
}
320336

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+
321346
try {
322347
for (const character of characters) {
323348
await startAgent(character, directClient as DirectClient);
@@ -384,3 +409,174 @@ async function handleUserInput(input, agentId) {
384409
console.error("Error fetching response:", error);
385410
}
386411
}
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+
}

docs/docs/guides/template-configuration.md

+12-10
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ Here are all the template options you can configure:
1515
```json
1616
{
1717
"templates": {
18-
"goalsTemplate": "", // Define character goals
19-
"factsTemplate": "", // Specify character knowledge
20-
"messageHandlerTemplate": "", // Handle general messages
21-
"shouldRespondTemplate": "", // Control response triggers
18+
"goalsTemplate": "", // Define character goals
19+
"factsTemplate": "", // Specify character knowledge
20+
"messageHandlerTemplate": "", // Handle general messages
21+
"shouldRespondTemplate": "", // Control response triggers
2222
"continueMessageHandlerTemplate": "", // Manage conversation flow
23-
"evaluationTemplate": "", // Handle response evaluation
24-
"twitterSearchTemplate": "", // Process Twitter searches
25-
"twitterPostTemplate": "", // Format Twitter posts
23+
"evaluationTemplate": "", // Handle response evaluation
24+
"twitterSearchTemplate": "", // Process Twitter searches
25+
"twitterPostTemplate": "", // Format Twitter posts
2626
"twitterMessageHandlerTemplate": "", // Handle Twitter messages
2727
"twitterShouldRespondTemplate": "", // Control Twitter responses
2828
"telegramMessageHandlerTemplate": "", // Handle Telegram messages
@@ -60,11 +60,11 @@ Configure platform-specific behaviors for your character, such as handling direc
6060
"clientConfig": {
6161
"telegram": {
6262
"shouldIgnoreDirectMessages": true, // Ignore DMs
63-
"shouldIgnoreBotMessages": true // Ignore bot messages
63+
"shouldIgnoreBotMessages": true // Ignore bot messages
6464
},
6565
"discord": {
66-
"shouldIgnoreBotMessages": true, // Ignore bot messages
67-
"shouldIgnoreDirectMessages": true // Ignore DMs
66+
"shouldIgnoreBotMessages": true, // Ignore bot messages
67+
"shouldIgnoreDirectMessages": true // Ignore DMs
6868
}
6969
}
7070
}
@@ -73,11 +73,13 @@ Configure platform-specific behaviors for your character, such as handling direc
7373
## Best Practices
7474

7575
1. **Template Management**
76+
7677
- Keep templates focused and specific
7778
- Use clear, consistent formatting
7879
- Document custom template behavior
7980

8081
2. **Client Configuration**
82+
8183
- Configure per platform as needed
8284
- Test behavior in development
8385
- Monitor interaction patterns

packages/adapter-postgres/schema.sql

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ CREATE TABLE IF NOT EXISTS accounts (
2626
"details" JSONB DEFAULT '{}'::jsonb
2727
);
2828

29+
CREATE TABLE IF NOT EXISTS characters (
30+
"id" UUID PRIMARY KEY,
31+
"name" TEXT,
32+
"characterState" JSONB NOT NULL,
33+
"secretsIV" JSONB DEFAULT '{}'::jsonb,
34+
"createdAt" TIMESTAMP DEFAULT now(),
35+
"updatedAt" TIMESTAMP DEFAULT now()
36+
);
37+
2938
CREATE TABLE IF NOT EXISTS rooms (
3039
"id" UUID PRIMARY KEY,
3140
"createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP

packages/adapter-postgres/src/index.ts

+56-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import {
99
type Relationship,
1010
type UUID,
1111
type IDatabaseCacheAdapter,
12+
type IDatabaseAdapter,
13+
type CharacterTable,
14+
type Secrets,
15+
type Character,
1216
Participant,
1317
DatabaseAdapter,
1418
elizaLogger,
@@ -22,7 +26,7 @@ const __dirname = path.dirname(__filename); // get the name of the directory
2226

2327
export class PostgresDatabaseAdapter
2428
extends DatabaseAdapter<Pool>
25-
implements IDatabaseCacheAdapter
29+
implements IDatabaseCacheAdapter, IDatabaseAdapter
2630
{
2731
private pool: Pool;
2832

@@ -39,6 +43,7 @@ export class PostgresDatabaseAdapter
3943
...defaultConfig,
4044
...connectionConfig, // Allow overriding defaults
4145
});
46+
this.db = this.pool;
4247

4348
this.pool.on("error", async (err) => {
4449
elizaLogger.error("Unexpected error on idle client", err);
@@ -736,7 +741,9 @@ export class PostgresDatabaseAdapter
736741
);
737742

738743
if (existingParticipant.rows.length > 0) {
739-
console.log(`Participant with userId ${userId} already exists in room ${roomId}.`);
744+
console.log(
745+
`Participant with userId ${userId} already exists in room ${roomId}.`
746+
);
740747
return; // Exit early if the participant already exists
741748
}
742749

@@ -750,11 +757,13 @@ export class PostgresDatabaseAdapter
750757
} catch (error) {
751758
// This is to prevent duplicate participant error in case of a race condition
752759
// Handle unique constraint violation error (code 23505)
753-
if (error.code === '23505') {
754-
console.warn(`Participant with userId ${userId} already exists in room ${roomId}.`); // Optionally, you can log this or handle it differently
760+
if (error.code === "23505") {
761+
console.warn(
762+
`Participant with userId ${userId} already exists in room ${roomId}.`
763+
); // Optionally, you can log this or handle it differently
755764
} else {
756765
// Handle other errors
757-
console.error('Error adding participant:', error);
766+
console.error("Error adding participant:", error);
758767
return false;
759768
}
760769
} finally {
@@ -958,6 +967,48 @@ export class PostgresDatabaseAdapter
958967
client.release();
959968
}
960969
}
970+
971+
/**
972+
* Loads characters from database
973+
* @param characterIds Optional array of character UUIDs to load
974+
* @returns Promise of tuple containing Characters array and their corresponding SecretsIV
975+
*/
976+
async loadCharacters(
977+
characterIds?: UUID[]
978+
): Promise<[Character[], Secrets[]]> {
979+
const client = await this.pool.connect();
980+
try {
981+
let query =
982+
'SELECT "id", "name", "characterState", "secretsIV" FROM characters';
983+
const queryParams: any[] = [];
984+
985+
if (characterIds?.length) {
986+
query += ' WHERE "id" = ANY($1)';
987+
queryParams.push(characterIds);
988+
}
989+
990+
query += " ORDER BY name";
991+
const result = await client.query<CharacterTable>(
992+
query,
993+
queryParams
994+
);
995+
996+
const characters: Character[] = [];
997+
const secretsIVs: Secrets[] = [];
998+
999+
for (const row of result.rows) {
1000+
characters.push(row.characterState);
1001+
secretsIVs.push(row.secretsIV || {});
1002+
}
1003+
1004+
return [characters, secretsIVs];
1005+
} catch (error) {
1006+
elizaLogger.error("Error loading characters:", error);
1007+
throw error;
1008+
} finally {
1009+
client.release();
1010+
}
1011+
}
9611012
}
9621013

9631014
export default PostgresDatabaseAdapter;

0 commit comments

Comments
 (0)