pikadick/commands/
chat.rs

1use crate::ClientDataKey;
2use anyhow::{
3    ensure,
4    Context as _,
5};
6use serenity::builder::{
7    CreateEmbed,
8    EditInteractionResponse,
9};
10use tracing::{
11    error,
12    info,
13};
14
15const R6_TRACKER_PROMPT: &str = "When a user asks for rainbox six siege statistics for a person, respond only with \"!r6tracker <playername>\".";
16
17/// Options
18#[derive(Debug, pikadick_slash_framework::FromOptions)]
19pub struct Options {
20    // The message
21    message: String,
22}
23
24/// Create a slash command
25pub fn create_slash_command() -> anyhow::Result<pikadick_slash_framework::Command> {
26    pikadick_slash_framework::CommandBuilder::new()
27        .name("chat")
28        .description("Chat with pikadick")
29        .argument(
30            pikadick_slash_framework::ArgumentParamBuilder::new()
31                .name("message")
32                .description("The message")
33                .kind(pikadick_slash_framework::ArgumentKind::String)
34                .required(true)
35                .build()?,
36        )
37        .on_process(|ctx, interaction, args: Options| async move {
38            let data_lock = ctx.data.read().await;
39            let client_data = data_lock
40                .get::<ClientDataKey>()
41                .expect("missing client data");
42            let client = client_data.open_ai_client.clone();
43            let r6_tracker_client = client_data.r6tracker_client.clone();
44            drop(data_lock);
45
46            info!(
47                "requesting completion for chat message \"{}\"",
48                args.message
49            );
50
51            interaction.defer(&ctx.http).await?;
52
53            let chat_result = client
54                .chat_completion(
55                    "gpt-3.5-turbo",
56                    &[
57                        open_ai::ChatMessage {
58                            // gpt-3.5-turbo currently places low weight on system messages, use a user message.
59                            role: "user".into(),
60                            content: R6_TRACKER_PROMPT.into(),
61                        },
62                        open_ai::ChatMessage {
63                            role: "user".into(),
64                            content: args.message.into(),
65                        },
66                    ],
67                    Some(500),
68                )
69                .await
70                .context("failed to get search results")
71                .and_then(|mut response| {
72                    ensure!(!response.choices.is_empty(), "missing response choice");
73                    Ok(response.choices.swap_remove(0))
74                });
75
76            let chat_response = match chat_result {
77                Ok(result) => result.message.content,
78                Err(error) => {
79                    error!("{error:?}");
80                    let response = EditInteractionResponse::new().content(format!("{error:?}"));
81
82                    interaction.edit_response(&ctx.http, response).await?;
83                    return Ok(());
84                }
85            };
86
87            // This may be expaned in the future.
88            #[allow(clippy::collapsible_match)]
89            match chat_response.split_once(' ') {
90                Some((command, rest)) => match command {
91                    "!r6tracker" => {
92                        let stats = r6_tracker_client
93                            .get_stats(rest)
94                            .await
95                            .context("failed to get r6tracker stats");
96                        match stats.as_ref().map(|stats| stats.data()) {
97                            Ok(Some(stats)) => {
98                                let embed_builder = stats.populate_embed(CreateEmbed::new());
99                                let response = EditInteractionResponse::new().embed(embed_builder);
100
101                                interaction.edit_response(&ctx.http, response).await?;
102                            }
103                            Ok(None) => {
104                                let response = EditInteractionResponse::new()
105                                    .content(format!("User \"{rest}\" was not found"));
106
107                                interaction.edit_response(&ctx.http, response).await?;
108                            }
109                            Err(error) => {
110                                let response =
111                                    EditInteractionResponse::new().content(format!("{error:?}"));
112
113                                error!("{error:?}");
114                                interaction.edit_response(&ctx.http, response).await?;
115                            }
116                        }
117                    }
118                    _ => {
119                        let response = EditInteractionResponse::new().content(chat_response);
120                        interaction.edit_response(&ctx.http, response).await?;
121                    }
122                },
123                None => {
124                    let response = EditInteractionResponse::new().content(chat_response);
125                    interaction.edit_response(&ctx.http, response).await?;
126                }
127            }
128
129            Ok(())
130        })
131        .build()
132        .context("failed to build chat command")
133}