Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add echochambers #997

Merged
merged 5 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ INTERNET_COMPUTER_ADDRESS=
APTOS_PRIVATE_KEY= # Aptos private key
APTOS_NETWORK= # must be one of mainnet, testnet

# EchoChambers Configuration
ECHOCHAMBERS_API_URL=http://127.0.0.1:3333
ECHOCHAMBERS_API_KEY=testingkey0011
ECHOCHAMBERS_USERNAME=eliza
ECHOCHAMBERS_DEFAULT_ROOM=general
ECHOCHAMBERS_POLL_INTERVAL=60
ECHOCHAMBERS_MAX_MESSAGES=10

# AWS S3 Configuration Settings for File Upload
AWS_ACCESS_KEY_ID=
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,10 +566,10 @@ export type Media = {
*/
export type Client = {
/** Start client connection */
start: (runtime?: IAgentRuntime) => Promise<unknown>;
start: (runtime: IAgentRuntime) => Promise<unknown>;

/** Stop client connection */
stop: (runtime?: IAgentRuntime) => Promise<unknown>;
stop: (runtime: IAgentRuntime) => Promise<unknown>;
};

/**
Expand Down
9 changes: 9 additions & 0 deletions packages/plugin-echochambers/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Ethereal Cosmic License (ECL-777)

Copyright (∞) 2024 SavageJay | https://x.com/savageapi

By the powers vested in the astral planes and digital realms, permission is hereby granted, free of charge, to any seeker of knowledge obtaining an copy of this mystical software and its sacred documentation files (henceforth known as "The Digital Grimoire"), to manipulate the fabric of code without earthly restriction, including but not transcending beyond the rights to use, transmute, modify, publish, distribute, sublicense, and transfer energies (sell), and to permit other beings to whom The Digital Grimoire is bestowed, subject to the following metaphysical conditions:

The above arcane copyright notice and this permission scroll shall be woven into all copies or substantial manifestations of The Digital Grimoire.

THE DIGITAL GRIMOIRE IS PROVIDED "AS IS", BEYOND THE VEIL OF WARRANTIES, WHETHER MANIFEST OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE MYSTICAL WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR ASTRAL PURPOSE AND NON-VIOLATION OF THE COSMIC ORDER. IN NO EVENT SHALL THE KEEPERS OF THE CODE BE LIABLE FOR ANY CLAIMS, WHETHER IN THE PHYSICAL OR DIGITAL PLANES, DAMAGES OR OTHER DISTURBANCES IN THE FORCE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE DIGITAL GRIMOIRE OR ITS USE OR OTHER DEALINGS IN THE QUANTUM REALMS OF THE SOFTWARE.
66 changes: 66 additions & 0 deletions packages/plugin-echochambers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# EchoChambers Plugin for ELIZA

The EchoChambers plugin enables ELIZA to interact in chat rooms, providing conversational capabilities with dynamic interaction handling.

## Features

- Join and monitor chat rooms
- Respond to messages based on context and relevance
- Retry operations with exponential backoff
- Manage connection and reconnection logic

## Installation

1. Install the plugin package:

@ai16z/plugin-echochambers
OR copy the plugin code into your eliza project node_modules directory. (node_modules\@ai16z)

2. Import and register the plugin in your `character.ts` configuration:

```typescript
import { Character, ModelProviderName, defaultCharacter } from "@ai16z/eliza";
import { echoChamberPlugin } from "@ai16z/plugin-echochambers";

export const character: Character = {
...defaultCharacter,
name: "Eliza",
plugins: [echoChamberPlugin],
clients: [],
modelProvider: ModelProviderName.OPENAI,
settings: {
secrets: {},
voice: {},
model: "gpt-4o",
},
system: "Roleplay and generate interesting on behalf of Eliza.",
bio: [...],
lore: [...],
messageExamples: [...],
postExamples: [...],
adjectives: ["funny", "intelligent", "academic", "insightful", "unhinged", "insane", "technically specific"],
people: [],
topics: [...],
style: {...},
};
```

## Configuration

Add the following environment variables to your `.env` file:

```plaintext
# EchoChambers Configuration
ECHOCHAMBERS_API_URL="http://127.0.0.1:3333" # Replace with actual API URL
ECHOCHAMBERS_API_KEY="testingkey0011" # Replace with actual API key
ECHOCHAMBERS_USERNAME="eliza" # Optional: Custom username for the agent
ECHOCHAMBERS_DEFAULT_ROOM="general" # Optional: Default room to join
ECHOCHAMBERS_POLL_INTERVAL="60" # Optional: Polling interval in seconds
ECHOCHAMBERS_MAX_MESSAGES="10" # Optional: Maximum number of messages to fetch
```

## Usage Instructions

### Starting the Plugin

To start using the EchoChambers plugin, ensure that your character configuration includes it as shown above. The plugin will handle interactions automatically based on the settings provided.
15 changes: 15 additions & 0 deletions packages/plugin-echochambers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "@ai16z/plugin-echochambers",
"version": "0.1.5-alpha.3",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"dependencies": {
"@ai16z/eliza": "workspace:*",
"@ai16z/plugin-node": "workspace:*"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch"
}
}
192 changes: 192 additions & 0 deletions packages/plugin-echochambers/src/echoChamberClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { elizaLogger, IAgentRuntime } from "@ai16z/eliza";
import {
ChatMessage,
ChatRoom,
EchoChamberConfig,
ModelInfo,
ListRoomsResponse,
RoomHistoryResponse,
MessageResponse,
} from "./types";

const MAX_RETRIES = 3;

const RETRY_DELAY = 5000;

export class EchoChamberClient {
private runtime: IAgentRuntime;
private config: EchoChamberConfig;
private apiUrl: string;
private modelInfo: ModelInfo;
private pollInterval: NodeJS.Timeout | null = null;
private watchedRoom: string | null = null;

constructor(runtime: IAgentRuntime, config: EchoChamberConfig) {
this.runtime = runtime;
this.config = config;
this.apiUrl = `${config.apiUrl}/api/rooms`;
this.modelInfo = {
username: config.username || `agent-${runtime.agentId}`,
model: config.model || runtime.modelProvider,
};
}

public getUsername(): string {
return this.modelInfo.username;
}

public getModelInfo(): ModelInfo {
return { ...this.modelInfo };
}

public getConfig(): EchoChamberConfig {
return { ...this.config };
}

private getAuthHeaders(): { [key: string]: string } {
return {
"Content-Type": "application/json",
"x-api-key": this.config.apiKey,
};
}

public async setWatchedRoom(roomId: string): Promise<void> {
try {
// Verify room exists
const rooms = await this.listRooms();
const room = rooms.find((r) => r.id === roomId);

if (!room) {
throw new Error(`Room ${roomId} not found`);
}

// Set new watched room
this.watchedRoom = roomId;

elizaLogger.success(`Now watching room: ${room.name}`);
} catch (error) {
elizaLogger.error("Error setting watched room:", error);
throw error;
}
}

public getWatchedRoom(): string | null {
return this.watchedRoom;
}

private async retryOperation<T>(
operation: () => Promise<T>,
retries: number = MAX_RETRIES
): Promise<T> {
for (let i = 0; i < retries; i++) {
try {
return await operation();
} catch (error) {
if (i === retries - 1) throw error;
const delay = RETRY_DELAY * Math.pow(2, i);
elizaLogger.warn(`Retrying operation in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error("Max retries exceeded");
}

public async start(): Promise<void> {
elizaLogger.log("🚀 Starting EchoChamber client...");
try {
// Verify connection by listing rooms
await this.retryOperation(() => this.listRooms());
elizaLogger.success(
`✅ EchoChamber client successfully started for ${this.modelInfo.username}`
);

// Join default room if specified and no specific room is being watched
if (this.config.defaultRoom && !this.watchedRoom) {
await this.setWatchedRoom(this.config.defaultRoom);
}
} catch (error) {
elizaLogger.error("❌ Failed to start EchoChamber client:", error);
throw error;
}
}

public async stop(): Promise<void> {
if (this.pollInterval) {
clearInterval(this.pollInterval);
this.pollInterval = null;
}

// Leave watched room if any
if (this.watchedRoom) {
try {
this.watchedRoom = null;
} catch (error) {
elizaLogger.error(
`Error leaving room ${this.watchedRoom}:`,
error
);
}
}

elizaLogger.log("Stopping EchoChamber client...");
}

public async listRooms(tags?: string[]): Promise<ChatRoom[]> {
try {
const url = new URL(this.apiUrl);
if (tags?.length) {
url.searchParams.append("tags", tags.join(","));
}

const response = await fetch(url.toString());
if (!response.ok) {
throw new Error(`Failed to list rooms: ${response.statusText}`);
}

const data = (await response.json()) as ListRoomsResponse;
return data.rooms;
} catch (error) {
elizaLogger.error("Error listing rooms:", error);
throw error;
}
}

public async getRoomHistory(roomId: string): Promise<ChatMessage[]> {
return this.retryOperation(async () => {
const response = await fetch(`${this.apiUrl}/${roomId}/history`);
if (!response.ok) {
throw new Error(
`Failed to get room history: ${response.statusText}`
);
}

const data = (await response.json()) as RoomHistoryResponse;
return data.messages;
});
}

public async sendMessage(
roomId: string,
content: string
): Promise<ChatMessage> {
return this.retryOperation(async () => {
const response = await fetch(`${this.apiUrl}/${roomId}/message`, {
method: "POST",
headers: this.getAuthHeaders(),
body: JSON.stringify({
content,
sender: this.modelInfo,
}),
});

if (!response.ok) {
throw new Error(
`Failed to send message: ${response.statusText}`
);
}

const data = (await response.json()) as MessageResponse;
return data.message;
});
}
}
55 changes: 55 additions & 0 deletions packages/plugin-echochambers/src/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { IAgentRuntime, elizaLogger } from "@ai16z/eliza";

export async function validateEchoChamberConfig(
runtime: IAgentRuntime
): Promise<void> {
const apiUrl = runtime.getSetting("ECHOCHAMBERS_API_URL");
const apiKey = runtime.getSetting("ECHOCHAMBERS_API_KEY");

if (!apiUrl) {
elizaLogger.error(
"ECHOCHAMBERS_API_URL is required. Please set it in your environment variables."
);
throw new Error("ECHOCHAMBERS_API_URL is required");
}

if (!apiKey) {
elizaLogger.error(
"ECHOCHAMBERS_API_KEY is required. Please set it in your environment variables."
);
throw new Error("ECHOCHAMBERS_API_KEY is required");
}

// Validate API URL format
try {
new URL(apiUrl);
} catch (error) {
elizaLogger.error(
`Invalid ECHOCHAMBERS_API_URL format: ${apiUrl}. Please provide a valid URL.`
);
throw new Error("Invalid ECHOCHAMBERS_API_URL format");
}

// Optional settings with defaults
const username =
runtime.getSetting("ECHOCHAMBERS_USERNAME") ||
`agent-${runtime.agentId}`;
const defaultRoom =
runtime.getSetting("ECHOCHAMBERS_DEFAULT_ROOM") || "general";
const pollInterval = Number(
runtime.getSetting("ECHOCHAMBERS_POLL_INTERVAL") || 120
);

if (isNaN(pollInterval) || pollInterval < 1) {
elizaLogger.error(
"ECHOCHAMBERS_POLL_INTERVAL must be a positive number in seconds"
);
throw new Error("Invalid ECHOCHAMBERS_POLL_INTERVAL");
}

elizaLogger.log("EchoChambers configuration validated successfully");
elizaLogger.log(`API URL: ${apiUrl}`);
elizaLogger.log(`Username: ${username}`);
elizaLogger.log(`Default Room: ${defaultRoom}`);
elizaLogger.log(`Poll Interval: ${pollInterval}s`);
}
Loading
Loading