Skip to content

Commit 2021bba

Browse files
Allow remote server control of characters
1 parent 908fff3 commit 2021bba

File tree

2 files changed

+321
-25
lines changed

2 files changed

+321
-25
lines changed

agent/src/index.ts

Lines changed: 299 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -369,26 +369,67 @@ export async function loadCharacters(
369369
return loadedCharacters;
370370
}
371371

372+
interface ReceivedCharacter {
373+
name: string;
374+
token_id: string;
375+
}
376+
377+
export async function loadCharactersFromServer(): Promise<Character[]> {
378+
const loadedCharacters: Character[] = [];
379+
try {
380+
const response = await fetch(`${process.env.CHARACTER_SERVER_URL}/fetch-characters`);
381+
if (!response.ok) {
382+
throw new Error(`HTTP error! status: ${response.status}`);
383+
}
384+
const characterData = await response.json();
385+
const characters: ReceivedCharacter[] = characterData.characterNames as ReceivedCharacter[];
386+
387+
// iterate over the characters and load them
388+
for (const char of characters) {
389+
const tokenId = char.token_id;
390+
const character = await loadCharacterFromServer(tokenId);
391+
loadedCharacters.push(character);
392+
}
393+
394+
} catch (error) {
395+
elizaLogger.error(`Error loading characters from server: ${error}`);
396+
elizaLogger.error(`Error details: ${error instanceof Error ? error.message : 'Unknown error'}`);
397+
elizaLogger.error(`Stack trace: ${error instanceof Error ? error.stack : 'No stack trace'}`);
398+
throw error;
399+
}
400+
401+
return loadedCharacters;
402+
}
403+
404+
async function loadCharacterFromServer(token_id: string): Promise<Character> {
405+
const response = await fetch(`${process.env.CHARACTER_SERVER_URL}/character/${token_id}`);
406+
const characterData = await response.json();
407+
const character: Character = characterData.character;
408+
return character;
409+
}
410+
372411
async function handlePluginImporting(plugins: string[]) {
373412
if (plugins.length > 0) {
374413
// this logging should happen before calling, so we can include important context
375414
//elizaLogger.info("Plugins are: ", plugins);
376415
const importedPlugins = await Promise.all(
377416
plugins.map(async (plugin) => {
378417
try {
379-
const importedPlugin:Plugin = await import(plugin);
418+
const importedPlugin: Plugin = await import(plugin);
380419
const functionName =
381420
plugin
382421
.replace("@elizaos/plugin-", "")
383422
.replace("@elizaos-plugins/plugin-", "")
384423
.replace(/-./g, (x) => x[1].toUpperCase()) +
385424
"Plugin"; // Assumes plugin function is camelCased with Plugin suffix
386425
if (!importedPlugin[functionName] && !importedPlugin.default) {
387-
elizaLogger.warn(plugin, 'does not have an default export or', functionName)
426+
elizaLogger.warn(plugin, 'does not have an default export or', functionName)
388427
}
389-
return {...(
390-
importedPlugin.default || importedPlugin[functionName]
391-
), npmName: plugin };
428+
return {
429+
...(
430+
importedPlugin.default || importedPlugin[functionName]
431+
), npmName: plugin
432+
};
392433
} catch (importError) {
393434
console.error(
394435
`Failed to import plugin: ${plugin}`,
@@ -709,23 +750,23 @@ function initializeCache(
709750
}
710751

711752
async function findDatabaseAdapter(runtime: AgentRuntime) {
712-
const { adapters } = runtime;
713-
let adapter: Adapter | undefined;
714-
// if not found, default to sqlite
715-
if (adapters.length === 0) {
716-
const sqliteAdapterPlugin = await import('@elizaos-plugins/adapter-sqlite');
717-
const sqliteAdapterPluginDefault = sqliteAdapterPlugin.default;
718-
adapter = sqliteAdapterPluginDefault.adapters[0];
719-
if (!adapter) {
720-
throw new Error("Internal error: No database adapter found for default adapter-sqlite");
721-
}
722-
} else if (adapters.length === 1) {
723-
adapter = adapters[0];
724-
} else {
725-
throw new Error("Multiple database adapters found. You must have no more than one. Adjust your plugins configuration.");
726-
}
727-
const adapterInterface = adapter?.init(runtime);
728-
return adapterInterface;
753+
const { adapters } = runtime;
754+
let adapter: Adapter | undefined;
755+
// if not found, default to sqlite
756+
if (adapters.length === 0) {
757+
const sqliteAdapterPlugin = await import('@elizaos-plugins/adapter-sqlite');
758+
const sqliteAdapterPluginDefault = sqliteAdapterPlugin.default;
759+
adapter = sqliteAdapterPluginDefault.adapters[0];
760+
if (!adapter) {
761+
throw new Error("Internal error: No database adapter found for default adapter-sqlite");
762+
}
763+
} else if (adapters.length === 1) {
764+
adapter = adapters[0];
765+
} else {
766+
throw new Error("Multiple database adapters found. You must have no more than one. Adjust your plugins configuration.");
767+
}
768+
const adapterInterface = adapter?.init(runtime);
769+
return adapterInterface;
729770
}
730771

731772
async function startAgent(
@@ -828,16 +869,152 @@ const handlePostCharacterLoaded = async (character: Character): Promise<Characte
828869
return processedCharacter;
829870
}
830871

872+
async function createAdminServer(directClient: DirectClient, charactersArg: string | undefined, adminPort: number) {
873+
const http = await import('http');
874+
const adminServer = http.createServer(async (req, res) => {
875+
res.setHeader('Content-Type', 'application/json');
876+
877+
// Check if request is from localhost
878+
const requestIP = req.socket.remoteAddress;
879+
if (!requestIP || !['127.0.0.1', '::1', '::ffff:127.0.0.1'].includes(requestIP)) {
880+
res.writeHead(403);
881+
res.end(JSON.stringify({ success: false, message: 'Access denied. Only localhost requests are allowed.' }));
882+
return;
883+
}
884+
885+
if (req.method === 'POST') {
886+
if (req.url === '/restart') {
887+
await restartAllAgents(directClient, charactersArg);
888+
res.writeHead(200);
889+
res.end(JSON.stringify({ success: true, message: 'All agents restarted successfully' }));
890+
} else if (req.url?.startsWith('/restart/')) {
891+
const characterName = req.url.split('/')[2]?.toLowerCase();
892+
if (characterName) {
893+
await restartSpecificCharacter(directClient, characterName, charactersArg);
894+
res.writeHead(200);
895+
res.end(JSON.stringify({ success: true, message: `Character ${characterName} restarted successfully` }));
896+
} else {
897+
res.writeHead(400);
898+
res.end(JSON.stringify({ success: false, message: 'Character name required' }));
899+
}
900+
} else if (req.url === '/add') {
901+
let body = '';
902+
req.on('data', chunk => {
903+
body += chunk.toString();
904+
});
905+
req.on('end', async () => {
906+
try {
907+
const { characterPath } = JSON.parse(body);
908+
if (!characterPath) {
909+
res.writeHead(400);
910+
res.end(JSON.stringify({ success: false, message: 'Character path is required' }));
911+
return;
912+
}
913+
await addNewCharacter(directClient, characterPath);
914+
res.writeHead(200);
915+
res.end(JSON.stringify({ success: true, message: 'Character added successfully' }));
916+
} catch (error) {
917+
res.writeHead(500);
918+
res.end(JSON.stringify({ success: false, message: error.message }));
919+
}
920+
});
921+
} else {
922+
res.writeHead(404);
923+
res.end(JSON.stringify({ success: false, message: 'Endpoint not found' }));
924+
}
925+
} else {
926+
res.writeHead(405);
927+
res.end(JSON.stringify({ success: false, message: 'Method not allowed' }));
928+
}
929+
});
930+
931+
// Start admin server
932+
adminServer.listen(adminPort, () => {
933+
elizaLogger.log(`Admin server listening on port ${adminPort}`);
934+
elizaLogger.log(`Available endpoints:`);
935+
elizaLogger.log(` POST http://localhost:${adminPort}/restart - Restart all agents`);
936+
elizaLogger.log(` POST http://localhost:${adminPort}/restart/{characterName} - Restart specific character`);
937+
elizaLogger.log(` POST http://localhost:${adminPort}/add - Add new character (requires characterPath in body)`);
938+
});
939+
940+
return adminServer;
941+
}
942+
831943
const startAgents = async () => {
832944
const directClient = new DirectClient();
833945
let serverPort = Number.parseInt(settings.SERVER_PORT || "3000");
946+
const adminPort = 8082; //TODO: move to .env
834947
const args = parseArguments();
835948
const charactersArg = args.characters || args.character;
836949
let characters = [defaultCharacter];
837950

838-
if ((charactersArg) || hasValidRemoteUrls()) {
839-
characters = await loadCharacters(charactersArg);
840-
}
951+
characters = await loadCharactersFromServer();
952+
953+
/*try {
954+
for (const character of characters) {
955+
const processedCharacter = await handlePostCharacterLoaded(character);
956+
await startAgent(processedCharacter, directClient);
957+
}
958+
// Add restart command handling to DirectClient's message system
959+
const originalHandleMessage = directClient.handleMessage;
960+
directClient.handleMessage = async (message) => {
961+
if (message.content === '/restart') {
962+
await restartAllAgents(directClient, charactersArg);
963+
return { type: 'success', content: 'All agents restarted successfully' };
964+
} else if (message.content.startsWith('/restart ')) {
965+
const characterName = message.content.split(' ')[1].toLowerCase();
966+
await restartSpecificCharacter(directClient, characterName, charactersArg);
967+
return { type: 'success', content: `Character ${characterName} restarted successfully` };
968+
}
969+
// Call the original handler for all other messages
970+
return originalHandleMessage.call(directClient, message);
971+
};
972+
973+
// Create and start the admin server
974+
await createAdminServer(directClient, charactersArg, adminPort);
975+
976+
// Listen for restart command from stdin
977+
process.stdin.on('data', async (data) => {
978+
const command = data.toString().trim();
979+
980+
if (command === 'restart') {
981+
await restartAllAgents(directClient, charactersArg);
982+
} else if (command.startsWith('restart ')) {
983+
const characterName = command.split(' ')[1].toLowerCase();
984+
await restartSpecificCharacter(directClient, characterName, charactersArg);
985+
}
986+
});
987+
988+
// Store the charactersArg for use in restarts
989+
directClient.charactersArg = charactersArg;
990+
991+
// Find available port
992+
while (!(await checkPortAvailable(serverPort))) {
993+
elizaLogger.warn(`Port ${serverPort} is in use, trying ${serverPort + 1}`);
994+
serverPort++;
995+
}
996+
997+
// upload some agent functionality into directClient
998+
directClient.startAgent = async (character) => {
999+
// Handle plugins
1000+
character.plugins = await handlePluginImporting(character.plugins);
1001+
return startAgent(character, directClient);
1002+
};
1003+
1004+
directClient.start(serverPort);
1005+
1006+
if (serverPort !== parseInt(settings.SERVER_PORT || "3000")) {
1007+
elizaLogger.log(`Server started on alternate port ${serverPort}`);
1008+
}
1009+
1010+
elizaLogger.log("Run `pnpm start:client` to start the client and visit the outputted URL (http://localhost:5173) to chat with your agents.");
1011+
elizaLogger.log("Type 'restart' and press Enter to restart all agents.");
1012+
} catch (error) {
1013+
elizaLogger.error("Error starting agents:", error);
1014+
}*/
1015+
1016+
// Create and start the admin server
1017+
await createAdminServer(directClient, charactersArg, adminPort);
8411018

8421019
try {
8431020
for (const character of characters) {
@@ -889,6 +1066,103 @@ const startAgents = async () => {
8891066
);
8901067
};
8911068

1069+
// Add these new functions above startAgents
1070+
async function restartAllAgents(directClient: DirectClient, charactersArg: string | undefined) {
1071+
try {
1072+
elizaLogger.info('Restarting all agents...');
1073+
1074+
// Stop existing agents
1075+
for (const [id, agent] of Object.entries(directClient.agents)) {
1076+
elizaLogger.info(`Stopping agent: ${id}`);
1077+
if (typeof agent.stop === 'function') {
1078+
await agent.stop();
1079+
}
1080+
}
1081+
1082+
// Clear all agents
1083+
directClient.agents = {};
1084+
1085+
const characters = await loadCharactersFromServer();
1086+
1087+
// Reload and restart agents
1088+
for (const character of characters) {
1089+
const processedCharacter = await handlePostCharacterLoaded(character);
1090+
elizaLogger.info(`Restarting agent for character: ${character.name}`);
1091+
await startAgent(processedCharacter, directClient);
1092+
}
1093+
elizaLogger.info('All agents restarted successfully');
1094+
} catch (error) {
1095+
elizaLogger.error('Error during restart:', error);
1096+
elizaLogger.info('Attempting to continue with existing agents...');
1097+
}
1098+
}
1099+
1100+
async function restartSpecificCharacter(directClient: DirectClient, characterTokenId: string, charactersArg: string | undefined) {
1101+
let characterName = '';
1102+
try {
1103+
// input should be tokenId
1104+
// fetch character from server
1105+
const character = await loadCharacterFromServer(characterTokenId);
1106+
1107+
// Process the character with post processors
1108+
const processedCharacter = await handlePostCharacterLoaded(character);
1109+
1110+
characterName = character.name;
1111+
1112+
// Find and stop the existing agent
1113+
let foundAgent = false;
1114+
for (const [id, agent] of Object.entries(directClient.agents)) {
1115+
elizaLogger.info(`Checking agent: ${id}`);
1116+
if (id.toLowerCase().includes(characterName.toLowerCase())) {
1117+
elizaLogger.info(`Stopping agent: ${id}`);
1118+
if (typeof agent.stop === 'function') {
1119+
await agent.stop();
1120+
}
1121+
delete directClient.agents[id];
1122+
foundAgent = true;
1123+
break;
1124+
}
1125+
}
1126+
1127+
if (!foundAgent) {
1128+
elizaLogger.warn(`No existing agent found for character: ${characterName}`);
1129+
}
1130+
1131+
// Start the new agent
1132+
elizaLogger.info(`Starting new agent for character: ${processedCharacter.name}`);
1133+
await startAgent(processedCharacter, directClient);
1134+
elizaLogger.info(`Character ${processedCharacter.name} restarted successfully`);
1135+
} catch (error) {
1136+
elizaLogger.error(`Error restarting character ${characterName}:`, error);
1137+
throw error; // Re-throw to be handled by caller
1138+
}
1139+
}
1140+
1141+
// Add this new function above startAgents
1142+
async function addNewCharacter(directClient: DirectClient, characterPath: string) {
1143+
try {
1144+
elizaLogger.info(`Adding new character from: ${characterPath}`);
1145+
1146+
// Load the new character
1147+
const newCharacter = (await loadCharacters(characterPath))[0];
1148+
if (!newCharacter) {
1149+
elizaLogger.error(`Failed to load character from file: ${characterPath}`);
1150+
return;
1151+
}
1152+
1153+
// Process the character with post processors
1154+
const processedCharacter = await handlePostCharacterLoaded(newCharacter);
1155+
1156+
// Start the new agent
1157+
elizaLogger.info(`Starting new agent for character: ${processedCharacter.name}`);
1158+
await startAgent(processedCharacter, directClient);
1159+
elizaLogger.info(`Character ${processedCharacter.name} added successfully`);
1160+
} catch (error) {
1161+
elizaLogger.error(`Error adding character from ${characterPath}:`, error);
1162+
throw error;
1163+
}
1164+
}
1165+
8921166
startAgents().catch((error) => {
8931167
elizaLogger.error("Unhandled error in startAgents:", error);
8941168
process.exit(1);

0 commit comments

Comments
 (0)