Skip to content

Commit

Permalink
Merge pull request #55 from caillef/discord
Browse files Browse the repository at this point in the history
feat: discord v0.1
  • Loading branch information
ponderingdemocritus authored Jan 27, 2025
2 parents 6f29044 + e5e2d23 commit 08d00e5
Show file tree
Hide file tree
Showing 7 changed files with 506 additions and 17 deletions.
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ STARKNET_PRIVATE_KEY=
# This is the current GraphQL to make the examples work
GRAPHQL_URL=https://api.cartridge.gg/x/sepolia-rc-18/torii

DRY_RUN=1
DRY_RUN=1

# Discord Bot Token (https://discord.com/developers/applications)
# Required Gateway Intents: Server Members, Message Content, Presence
DISCORD_TOKEN=
172 changes: 172 additions & 0 deletions examples/example-discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* Example demonstrating a Discord bot using the Daydreams package.
* This bot can:
* - Reply to DMs
*/

import { Orchestrator } from "../packages/core/src/core/orchestrator";
import { HandlerRole } from "../packages/core/src/core/types";
import { DiscordClient } from "../packages/core/src/core/io/discord";
import { RoomManager } from "../packages/core/src/core/room-manager";
import { ChromaVectorDB } from "../packages/core/src/core/vector-db";
import { MessageProcessor } from "../packages/core/src/core/processors/message-processor";
import { LLMClient } from "../packages/core/src/core/llm-client";
import { env } from "../packages/core/src/core/env";
import { LogLevel } from "../packages/core/src/core/types";
import chalk from "chalk";
import { defaultCharacter } from "../packages/core/src/core/character";
import { z } from "zod";
import readline from "readline";
import { MongoDb } from "../packages/core/src/core/mongo-db";

async function main() {
const loglevel = LogLevel.DEBUG;

// Initialize core dependencies
const vectorDb = new ChromaVectorDB("discord_agent", {
chromaUrl: "http://localhost:8000",
logLevel: loglevel,
});

await vectorDb.purge(); // Clear previous session data

const roomManager = new RoomManager(vectorDb);

const llmClient = new LLMClient({
model: "anthropic/claude-3-5-sonnet-latest", // Using a known supported model
temperature: 0.3,
});

// Initialize processor with default character personality
const processor = new MessageProcessor(
llmClient,
defaultCharacter,
loglevel
);

// Initialize core system
const scheduledTaskDb = new MongoDb(
"mongodb://localhost:27017",
"myApp",
"scheduled_tasks"
);

await scheduledTaskDb.connect();
console.log(chalk.green("✅ Scheduled task database connected"));

await scheduledTaskDb.deleteAll();

// Initialize core system
const core = new Orchestrator(
roomManager,
vectorDb,
[processor],
scheduledTaskDb,
{
level: loglevel,
enableColors: true,
enableTimestamp: true,
}
);

function messageCreate(bot: any, message: any) {
const isMention =
message.mentions.users.findKey(
(user: any) => user.id === bot.id
) !== undefined;
if (isMention) {
core.dispatchToInput(
"discord_mention",
{
content: message.content,
sentBy: message.author.id,
channelId: message.channelId,
},
message.author.id
);
}
}

// Set up Discord client with credentials
const discord = new DiscordClient(
{
discord_token: env.DISCORD_TOKEN,
},
loglevel,
{
messageCreate,
}
);

// Register input handler for Discord mentions
core.registerIOHandler({
name: "discord_mention",
role: HandlerRole.INPUT,
handler: async (data: unknown) => {
const message = data as { content: string; sentBy: string };
console.log(chalk.blue("🔍 Received Discord mention..."));

return [message];
},
schema: z.object({
sentBy: z.string(),
content: z.string(),
channelId: z.string(),
}),
});

// Register output handler for Discord replies
core.registerIOHandler({
name: "discord_reply",
role: HandlerRole.OUTPUT,
handler: async (data: unknown) => {
const messageData = data as {
content: string;
channelId: string;
};
return discord.createMessageOutput().handler(messageData);
},
schema: z
.object({
content: z.string(),
channelId: z
.string()
.optional()
.describe("The channel ID of the message"),
})
.describe(
"If you have been tagged or mentioned in Discord, use this. This is for replying to a message."
),
});

// Set up readline interface
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});

// Start the prompt loop
console.log(chalk.cyan("🤖 Bot is now running and monitoring Discord..."));
console.log(chalk.cyan("You can type messages in the console."));
console.log(chalk.cyan('Type "exit" to quit'));

// Handle graceful shutdown
process.on("SIGINT", async () => {
console.log(chalk.yellow("\n\nShutting down..."));

// Clean up resources
discord.destroy();
core.removeIOHandler("discord_mention");
core.removeIOHandler("discord_reply");
rl.close();

console.log(chalk.green("✅ Shutdown complete"));
process.exit(0);
});
}

// Run the example
main().catch((error) => {
console.error(chalk.red("Fatal error:"), error);
process.exit(1);
});
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"task": "bun run examples/example-basic.ts",
"twitter": "bun run examples/example-twitter.ts",
"server": "bun run examples/example-server.ts",
"discord": "bun run examples/example-discord.ts",
"api": "bun run examples/example-api.ts",
"ui": "pnpm --dir clients/example-ui run dev",
"test": "bun run packages/core",
Expand All @@ -21,6 +22,7 @@
"chalk": "^5.4.1",
"cors": "^2.8.5",
"express": "^4.21.2",
"discord.js": "^14.17.3",
"lerna": "^8.1.9",
"mongodb": "^6.12.0",
"prettier": "^3.4.2",
Expand Down
5 changes: 2 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"ai": "^4.1.0",
"ajv": "^8.17.1",
"bs58": "^6.0.0",
"chromadb": "^1.9.4",
"chromadb": "^1.10.4",
"chromadb-default-embed": "^2.13.2",
"ethers": "^6.13.5",
"mongodb": "^6.12.0",
Expand All @@ -49,5 +49,4 @@
"zod": "^3.24.1",
"zod-to-json-schema": "^3.24.1"
}
}

}
1 change: 1 addition & 0 deletions packages/core/src/core/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const envSchema = z.object({
STARKNET_PRIVATE_KEY: z.string(),
OPENROUTER_API_KEY: z.string(),
GRAPHQL_URL: z.string(),
DISCORD_TOKEN: z.string(),
WEBSOCKET_URL: z.string().default("ws://localhost:8080"),
DRY_RUN: z
.preprocess((val) => val === "1" || val === "true", z.boolean())
Expand Down
160 changes: 160 additions & 0 deletions packages/core/src/core/io/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Client, Events, GatewayIntentBits, Partials, User } from "discord.js";
import type { JSONSchemaType } from "ajv";
import { Logger } from "../../core/logger";
import { LogLevel } from "../types";
import { env } from "../../core/env";

export interface DiscordCredentials {
discord_token: string;
}

export interface MessageData {
content: string;
channelId: string;
conversationId?: string;
sendBy?: string;
}

export interface EventCallbacks {
messageCreate?: (bot: any, message: any) => void;
}

// Schema for message output validation
export const messageSchema: JSONSchemaType<MessageData> = {
type: "object",
properties: {
content: { type: "string" },
channelId: { type: "string" },
sendBy: { type: "string", nullable: true },
conversationId: { type: "string", nullable: true },
},
required: ["content", "channelId"],
additionalProperties: false,
};

export class DiscordClient {
private client: Client;
private logger: Logger;

constructor(
private credentials: DiscordCredentials,
logLevel: LogLevel = LogLevel.INFO,
eventCallbacks: EventCallbacks
) {
this.client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.DirectMessageTyping,
GatewayIntentBits.DirectMessageReactions,
],
partials: [Partials.Channel], // Enable DM
});
this.credentials = credentials;
this.logger = new Logger({
level: logLevel,
enableColors: true,
enableTimestamp: true,
});

if (eventCallbacks.messageCreate) {
this.client.on(Events.MessageCreate, (message) => {
if (eventCallbacks.messageCreate) {
eventCallbacks.messageCreate(this.client.user, message);
}
});
}

this.client.on(Events.ClientReady, async () => {
this.logger.info("DiscordClient", "Initialized successfully");
});

this.client.login(this.credentials.discord_token).catch((error) => {
this.logger.error("DiscordClient", "Failed to login", { error });
console.error("Login error:", error);
});
}

public destroy() {
this.client.destroy();
}

/**
* Create an output for sending messages
*/
public createMessageOutput() {
return {
name: "discord_message",
handler: async (data: MessageData) => {
return await this.sendMessage(data);
},
response: {
success: "boolean",
channelId: "string",
},
schema: messageSchema,
};
}

private async sendMessage(data: MessageData) {
try {
this.logger.info(
"DiscordClient.sendMessage",
"Would send message",
{
data,
}
);

if (env.DRY_RUN) {
return {
success: true,
channelId: "DRY RUN CHANNEL ID",
};
}

const channel = this.client.channels.cache.get(data.channelId);
if (!channel?.isTextBased()) {
const error = new Error(
`Invalid or unsupported channel: ${data.channelId}`
);
this.logger.error(
"DiscordClient.sendMessage",
"Error sending message",
{
error,
}
);
throw error;
}
const sentMessage = await channel.send(data.content);

return {
success: true,
messageId: sentMessage.id,
};
} catch (error) {
this.logger.error(
"DiscordClient.sendMessage",
"Error sending message",
{
error,
}
);
throw error;
}
}
}

// Example usage:
/*
const discord = new DiscordClient({
discord_token: "M..."
});
// Register output
core.registerOutput(discord.createMessageOutput());
*/
Loading

0 comments on commit 08d00e5

Please sign in to comment.