rig_agent/integrations/
discord_bot.rs1use crate::agent::Agent;
4use crate::completion::Chat;
5use rig_core::message::Message as RigMessage;
6use serenity::all::{
7 Command, CommandInteraction, Context, CreateCommand, CreateThread, EventHandler,
8 GatewayIntents, Interaction, Message, Ready, async_trait,
9};
10use std::collections::HashMap;
11use std::env;
12use std::sync::Arc;
13use thiserror::Error;
14use tokio::sync::RwLock;
15
16#[derive(Debug, Error)]
17pub enum DiscordBotError {
18 #[error("Discord bot token missing from environment: {0}")]
19 MissingToken(#[from] env::VarError),
20 #[error("Failed to build Discord client: {0}")]
21 ClientBuild(#[from] serenity::Error),
22}
23
24struct BotState {
26 agent: Agent,
27 conversations: Arc<RwLock<HashMap<u64, Vec<RigMessage>>>>,
28}
29
30impl BotState {
31 fn new(agent: Agent) -> Self {
32 Self {
33 agent,
34 conversations: Arc::new(RwLock::new(HashMap::new())),
35 }
36 }
37}
38
39struct Handler {
41 state: Arc<BotState>,
42}
43
44#[async_trait]
45impl EventHandler for Handler {
46 async fn ready(&self, ctx: Context, ready: Ready) {
47 println!("{} is connected!", ready.user.name);
48
49 let register_cmd =
50 CreateCommand::new("new").description("Start a new chat session with the bot");
51
52 let command = Command::create_global_command(&ctx.http, register_cmd).await;
54
55 match command {
56 Ok(cmd) => println!("Registered global command: {}", cmd.name),
57 Err(e) => eprintln!("Failed to register command: {}", e),
58 }
59 }
60
61 async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
62 if let Interaction::Command(command) = interaction {
63 self.handle_command(&ctx, &command).await;
64 }
65 }
66
67 async fn message(&self, ctx: Context, msg: Message) {
68 if msg.author.bot {
70 return;
71 }
72
73 let conversations = self.state.conversations.read().await;
75 if conversations.contains_key(&msg.channel_id.get()) {
76 drop(conversations);
77 self.handle_thread_message(&ctx, &msg).await;
78 }
79 }
80}
81
82impl Handler {
83 async fn handle_command(&self, ctx: &Context, command: &CommandInteraction) {
84 if command.data.name.as_str() == "new" {
85 if let Err(e) = command.defer(&ctx.http).await {
87 eprintln!("Failed to defer command: {}", e);
88 return;
89 }
90
91 let thread_name = format!("AI Conversation - {}", command.user.name);
93
94 let thread = match command
95 .channel_id
96 .create_thread(
97 &ctx.http,
98 CreateThread::new(thread_name)
99 .kind(serenity::all::ChannelType::PublicThread)
100 .auto_archive_duration(serenity::all::AutoArchiveDuration::OneDay),
101 )
102 .await
103 {
104 Ok(t) => t,
105 Err(e) => {
106 eprintln!("Failed to create thread: {}", e);
107 let _ = command
108 .edit_response(
109 &ctx.http,
110 serenity::all::EditInteractionResponse::new()
111 .content("Failed to create thread. Please try again."),
112 )
113 .await;
114 return;
115 }
116 };
117
118 let mut conversations = self.state.conversations.write().await;
120 conversations.insert(thread.id.get(), Vec::new());
121 drop(conversations);
122
123 if let Err(e) = command
125 .edit_response(
126 &ctx.http,
127 serenity::all::EditInteractionResponse::new()
128 .content(format!(
129 "Started a new conversation in <#{}>! Send messages there to chat with the AI.",
130 thread.id
131 ))
132 )
133 .await
134 {
135 eprintln!("Failed to edit response: {}", e);
136 }
137
138 if let Err(e) = thread
140 .send_message(
141 &ctx.http,
142 serenity::all::CreateMessage::new()
143 .content("Hello! I'm ready to help. What would you like to talk about?"),
144 )
145 .await
146 {
147 eprintln!("Failed to send welcome message: {}", e);
148 }
149 }
150 }
151
152 async fn handle_thread_message(&self, ctx: &Context, msg: &Message) {
153 let thread_id = msg.channel_id.get();
154
155 let _ = msg.channel_id.broadcast_typing(&ctx.http).await;
157
158 let conversations = self.state.conversations.read().await;
160 let mut history = if let Some(history) = conversations.get(&thread_id) {
161 history.clone()
162 } else {
163 vec![]
164 };
165 drop(conversations);
166
167 let response = match self.state.agent.chat(&msg.content, &mut history).await {
170 Ok(resp) => resp,
171 Err(e) => {
172 eprintln!("Agent error: {}", e);
173 let _ = msg
174 .channel_id
175 .say(
176 &ctx.http,
177 "Sorry, I encountered an error processing your message.",
178 )
179 .await;
180 return;
181 }
182 };
183
184 {
186 let mut conversations = self.state.conversations.write().await;
187 conversations.insert(thread_id, history);
188 }
189
190 let chunks: Vec<String> = response
192 .chars()
193 .collect::<Vec<_>>()
194 .chunks(1900)
195 .map(|c| c.iter().collect())
196 .collect();
197
198 for chunk in chunks {
199 if let Err(e) = msg.channel_id.say(&ctx.http, &chunk).await {
200 eprintln!("Failed to send message: {}", e);
201 }
202 }
203 }
204}
205
206pub trait DiscordExt: Sized + Send + Sync
209where
210 Self: 'static,
211{
212 fn into_discord_bot(
213 self,
214 token: &str,
215 ) -> impl std::future::Future<Output = Result<serenity::Client, DiscordBotError>> + Send;
216
217 fn into_discord_bot_from_env(
218 self,
219 ) -> impl std::future::Future<Output = Result<serenity::Client, DiscordBotError>> + Send {
220 async move {
221 let token = std::env::var("DISCORD_BOT_TOKEN")?;
222 DiscordExt::into_discord_bot(self, &token).await
223 }
224 }
225}
226
227impl DiscordExt for Agent {
228 async fn into_discord_bot(self, token: &str) -> Result<serenity::Client, DiscordBotError> {
229 let intents = GatewayIntents::GUILDS
230 | GatewayIntents::GUILD_MESSAGES
231 | GatewayIntents::MESSAGE_CONTENT;
232
233 let state = Arc::new(BotState::new(self));
234 let handler = Handler {
235 state: state.clone(),
236 };
237
238 serenity::Client::builder(token, intents)
239 .event_handler(handler)
240 .await
241 .map_err(DiscordBotError::from)
242 }
243}