diff --git a/agent/src/index.ts b/agent/src/index.ts index 05fbc92856..d3f1a0276e 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -148,6 +148,61 @@ function tryLoadFile(filePath: string): string | null { return null; } } +function mergeCharacters(base: Character, child: Character): Character { + const mergeObjects = (baseObj: any, childObj: any) => { + const result: any = {}; + const keys = new Set([...Object.keys(baseObj || {}), ...Object.keys(childObj || {})]); + keys.forEach(key => { + if (typeof baseObj[key] === 'object' && typeof childObj[key] === 'object' && !Array.isArray(baseObj[key]) && !Array.isArray(childObj[key])) { + result[key] = mergeObjects(baseObj[key], childObj[key]); + } else if (Array.isArray(baseObj[key]) || Array.isArray(childObj[key])) { + result[key] = [...(baseObj[key] || []), ...(childObj[key] || [])]; + } else { + result[key] = childObj[key] !== undefined ? childObj[key] : baseObj[key]; + } + }); + return result; + }; + return mergeObjects(base, child); +} +async function loadCharacter(filePath: string): Promise { + const content = tryLoadFile(filePath); + if (!content) { + throw new Error(`Character file not found: ${filePath}`); + } + let character = JSON.parse(content); + validateCharacterConfig(character); + + // .id isn't really valid + const characterId = character.id || character.name; + const characterPrefix = `CHARACTER.${characterId.toUpperCase().replace(/ /g, "_")}.`; + const characterSettings = Object.entries(process.env) + .filter(([key]) => key.startsWith(characterPrefix)) + .reduce((settings, [key, value]) => { + const settingKey = key.slice(characterPrefix.length); + return { ...settings, [settingKey]: value }; + }, {}); + if (Object.keys(characterSettings).length > 0) { + character.settings = character.settings || {}; + character.settings.secrets = { + ...characterSettings, + ...character.settings.secrets, + }; + } + // Handle plugins + character.plugins = await handlePluginImporting( + character.plugins + ); + if (character.extends) { + elizaLogger.info(`Merging ${character.name} character with parent characters`); + for (const extendPath of character.extends) { + const baseCharacter = await loadCharacter(path.resolve(path.dirname(filePath), extendPath)); + character = mergeCharacters(baseCharacter, character); + elizaLogger.info(`Merged ${character.name} with ${baseCharacter.name}`); + } + } + return character; +} export async function loadCharacters( charactersArg: string @@ -211,32 +266,7 @@ export async function loadCharacters( } try { - const character = JSON.parse(content); - validateCharacterConfig(character); - - // .id isn't really valid - const characterId = character.id || character.name; - const characterPrefix = `CHARACTER.${characterId.toUpperCase().replace(/ /g, "_")}.`; - - const characterSettings = Object.entries(process.env) - .filter(([key]) => key.startsWith(characterPrefix)) - .reduce((settings, [key, value]) => { - const settingKey = key.slice(characterPrefix.length); - return { ...settings, [settingKey]: value }; - }, {}); - - if (Object.keys(characterSettings).length > 0) { - character.settings = character.settings || {}; - character.settings.secrets = { - ...characterSettings, - ...character.settings.secrets, - }; - } - - // Handle plugins - character.plugins = await handlePluginImporting( - character.plugins - ); + const character: Character = await loadCharacter(resolvedPath); loadedCharacters.push(character); elizaLogger.info( diff --git a/packages/core/src/defaultCharacter.ts b/packages/core/src/defaultCharacter.ts index e4a81b07e2..8faaa64f2b 100644 --- a/packages/core/src/defaultCharacter.ts +++ b/packages/core/src/defaultCharacter.ts @@ -527,4 +527,5 @@ export const defaultCharacter: Character = { "meticulous", "provocative", ], + extends: [], }; diff --git a/packages/core/src/environment.ts b/packages/core/src/environment.ts index ed7edf3bf2..4bbe5fcb91 100644 --- a/packages/core/src/environment.ts +++ b/packages/core/src/environment.ts @@ -135,6 +135,7 @@ export const CharacterSchema = z.object({ prompt: z.string().optional(), }) .optional(), + extends: z.array(z.string()).optional(), }); // Type inference diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 9690c1a0d4..2c958c6993 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -872,6 +872,8 @@ export type Character = { nft?: { prompt: string; }; + /**Optinal Parent characters to inherit information from */ + extends?: string[]; }; /**