Skip to content
Open
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
76 changes: 76 additions & 0 deletions src/context.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use std::fmt::Display;

use chrono::{Local, NaiveDate};
use serenity::all::{Context, Message, PartialGuild};

#[non_exhaustive]
pub struct MessageContext<'a> {
pub server_name: &'a str,
pub content: &'a str,
pub date: NaiveDate,
}

impl<'a> MessageContext<'a> {
pub async fn from(guild: &'a PartialGuild, _ctx: &'a Context, msg: &'a Message) -> Self {
// The bot accepts two inputs
// 1. A message with information with mentions it with an @CalBot
// 2. Replying to a message with information and mentioning @CalBot in the reply
let (content, date) = match msg.referenced_message {
Some(ref message) => {
if let Some(edited) = message.edited_timestamp {
(&message.content, edited.date_naive())
} else {
(&message.content, message.timestamp.date_naive())
}
}
None => (&msg.content, msg.timestamp.date_naive()),
};

Self {
server_name: &guild.name,
content,
date,
}
}
}

impl<'a> Display for MessageContext<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
include_str!("./messages/message-context-format.txt"),
self.server_name, self.content,
)
}
}

impl<'a> Default for MessageContext<'a> {
fn default() -> Self {
Self {
server_name: "Unknown Server",
content: "",
date: Local::now().date_naive(),
}
}
}

#[cfg(test)]
mod test {
use super::MessageContext;

#[test]
fn test_format() {
let ctx = MessageContext {
server_name: "Test Server",
content: "Some random message.",
date: chrono::NaiveDate::from_ymd_opt(2023, 10, 1).unwrap(),
};

let res = format!("{}", ctx);

assert_eq!(
res,
"Context for the message:\n- Server Name: Test Server\n- Content: Some random message.\n"
);
}
}
17 changes: 4 additions & 13 deletions src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serenity::{
};

use crate::{
context::MessageContext,
parser::{parse_msg, Error},
utils::{calendar_message, upload_calendar},
};
Expand Down Expand Up @@ -51,19 +52,9 @@ impl EventHandler for Handler {
return;
}

// The bot accepts two inputs
// 1. A message with information with mentions it with an @CalBot
// 2. Replying to a message with information and mentioning @CalBot in the reply
let res = match msg.referenced_message {
Some(ref ref_msg) => {
if let Some(edited) = ref_msg.edited_timestamp {
parse_msg(&ref_msg.content, &edited.date_naive()).await
} else {
parse_msg(&ref_msg.content, &ref_msg.timestamp.date_naive()).await
}
}
None => parse_msg(&msg.content, &msg.timestamp.date_naive()).await,
};
let context = MessageContext::from(&guild, &ctx, &msg).await;

let res = parse_msg(&context).await;

match res {
Ok(calendar) => {
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod events;
mod parser;
mod utils;
mod context;
use shuttle_runtime::SecretStore;

use events::Handler;
Expand Down
4 changes: 0 additions & 4 deletions src/llm-prompt.txt → src/messages/llm-prompt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,3 @@ Try to keep the title brief, no more than 5 words.
If the message does not seem to be parseable, return an empty string.

If there are no times in the message, do not attempt to guess the time. If there are no dates in the message, do not attempt to guess the date.

Message:


3 changes: 3 additions & 0 deletions src/messages/message-context-format.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Context for the message:
- Server Name: {}
- Content: {}
98 changes: 72 additions & 26 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ use icalendar::{Calendar, Component, Event, EventLike};
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use serde::Deserialize;

use crate::context::MessageContext;

const GROQ_ENDPOINT: &str = "https://api.groq.com/openai/v1/chat/completions";
const PROMPT_INSTRUCTIONS: &str = include_str!("llm-prompt.txt");
const SYSTEM_PROMPT: &str = include_str!("./messages/llm-prompt.txt");
const MAX_COMPLETION_TOKEN: usize = 300;

#[derive(Deserialize, Debug)]
Expand Down Expand Up @@ -97,19 +99,23 @@ fn parse_date(date_str: &str, msg_date: &NaiveDate) -> Result<NaiveDate, Error>
}
}

pub async fn parse_msg(msg: &str, message_date: &NaiveDate) -> Result<Calendar, Error> {
pub async fn parse_msg<'a>(ctx: &MessageContext<'a>) -> Result<Calendar, Error> {
let groq_key = std::env::var("GROQ_API_KEY").expect("GROQ_API_KEY missing");

let full_prompt = [PROMPT_INSTRUCTIONS, msg].join("\r\n");
let prompt = format!("{}", ctx);

let req_body = serde_json::json!({
"model": "llama-3.3-70b-versatile",
// "model": "llama-3.2-90b-vision-preview",
"max_completion_tokens": MAX_COMPLETION_TOKEN,
"messages": [
{
"role": "system",
"content": SYSTEM_PROMPT,
},
{
"role": "user",
"content": full_prompt,
"content": prompt,
}
]});

Expand Down Expand Up @@ -141,7 +147,7 @@ pub async fn parse_msg(msg: &str, message_date: &NaiveDate) -> Result<Calendar,

let groq_output: GroqOutput = toml::from_str(output).map_err(|_| Error::ParseFailure)?;
groq_output
.to_ical(message_date)
.to_ical(&ctx.date)
.map_err(|_| Error::ParseFailure)
}

Expand Down Expand Up @@ -240,17 +246,24 @@ mod tests {
#[tokio::test]
#[ignore]
async fn mock_irrelevant_input() {
let msg = "69420";
let res = parse_msg(msg, &Local::now().date_naive()).await;
let ctx = MessageContext {
content: "69420",
..Default::default()
};
let res = parse_msg(&ctx).await;
assert!(matches!(res, Err(Error::ParseFailure)));
}

#[tokio::test]
#[ignore]
async fn mock_today_date() {
let msg = "ACM Club is meeting today from 4-6pm in OCNL 241!";
let date = Local::now().date_naive();
let res = parse_msg(msg, &date).await;
let ctx = MessageContext {
server_name: "ACM",
content: "ACM Club is meeting today from 4-6pm in OCNL 241!",
date,
};
let res = parse_msg(&ctx).await;

assert!(res.is_ok());
let calendar = res.unwrap();
Expand Down Expand Up @@ -297,9 +310,13 @@ mod tests {
#[tokio::test]
#[ignore]
async fn mock_tmrw_historical_leap_year() {
let msg = "ACM Club is meeting tomorrow from 4-6pm in OCNL 241!";
let date = NaiveDate::from_ymd_opt(2024, 2, 28).unwrap();
let res = parse_msg(msg, &date).await;
let ctx = MessageContext {
server_name: "ACM",
content: "ACM Club is meeting tomorrow from 4-6pm in OCNL 241!",
date,
};
let res = parse_msg(&ctx).await;

assert!(res.is_ok());
let calendar = res.unwrap();
Expand Down Expand Up @@ -348,7 +365,12 @@ mod tests {
async fn mock_2_days_historical_leap_year() {
let msg = "ACM Club is meeting in two days from 4-6pm in OCNL 241!";
let final_date = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
let res = parse_msg(msg, &final_date.checked_sub_days(Days::new(2)).unwrap()).await;
let ctx = MessageContext {
server_name: "ACM",
content: msg,
date: final_date.checked_sub_days(Days::new(2)).unwrap(),
};
let res = parse_msg(&ctx).await;

assert!(res.is_ok());
let calendar = res.unwrap();
Expand Down Expand Up @@ -395,9 +417,13 @@ mod tests {
#[tokio::test]
#[ignore]
async fn mock_5_days_historical_leap_year() {
let msg = "ACM Club is meeting in five days from 5-7pm in OCNL 241!";
let final_date = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
let res = parse_msg(msg, &final_date.checked_sub_days(Days::new(5)).unwrap()).await;
let ctx = MessageContext {
server_name: "ACM",
content: "ACM Club is meeting in five days from 5-7pm in OCNL 241!",
date: final_date.checked_sub_days(Days::new(5)).unwrap(),
};
let res = parse_msg(&ctx).await;

assert!(res.is_ok());
let calendar = res.unwrap();
Expand Down Expand Up @@ -444,9 +470,13 @@ mod tests {
#[tokio::test]
#[ignore]
async fn mock_missing_end_time() {
let msg = "ACM Club is meeting in tomorrow at 4pm in OCNL 241!";
let date = NaiveDate::from_ymd_opt(2021, 6, 9).unwrap();
let res = parse_msg(msg, &date).await;
let ctx = MessageContext {
server_name: "ACM",
content: "ACM Club is meeting in tomorrow at 4pm in OCNL 241!",
date,
};
let res = parse_msg(&ctx).await;

assert!(res.is_ok());
let calendar = res.unwrap();
Expand Down Expand Up @@ -496,9 +526,13 @@ mod tests {
#[tokio::test]
#[ignore]
async fn mock_exact_date() {
let msg = "ACM Club is meeting on 10/31 from 11:30-2:45pm in the Mechoopda Dorms";
let date = NaiveDate::from_ymd_opt(2009, 6, 9).unwrap();
let res = parse_msg(msg, &date).await;
let ctx = MessageContext {
server_name: "ACM",
content: "ACM Club is meeting on 10/31 from 11:30-2:45pm in the Mechoopda Dorms",
date,
};
let res = parse_msg(&ctx).await;

assert!(res.is_ok());
let calendar = res.unwrap();
Expand Down Expand Up @@ -554,9 +588,13 @@ mod tests {
#[tokio::test]
#[ignore]
async fn real_usr0_1_28_25() {
let msg = "Hey @everyone Voting has concluded and it has been decided that our meeting time this semester will be Mondays from 5-6 in OCNL 239. Our first meeting will be next Monday where we will be discussing the schedule for the upcoming semester, and doing some intro into hacking and cybersecurity.";
let date = NaiveDate::from_ymd_opt(2025, 1, 28).unwrap();
let res = parse_msg(msg, &date).await;
let ctx = MessageContext {
server_name: "TEST",
content: "Hey @everyone Voting has concluded and it has been decided that our meeting time this semester will be Mondays from 5-6 in OCNL 239. Our first meeting will be next Monday where we will be discussing the schedule for the upcoming semester, and doing some intro into hacking and cybersecurity.",
date
};
let res = parse_msg(&ctx).await;

assert!(res.is_ok());
let calendar = res.unwrap();
Expand Down Expand Up @@ -612,9 +650,13 @@ mod tests {
#[tokio::test]
#[ignore]
async fn real_tpc_2_3_25() {
let msg = "@everyone It is my great pleasure to announce TPC's first meeting of the semester! Join us this Thursday, at 5 PM in OCNL 241 for an inspiring talk on careers, life, and projects by *James Krepelka*, an experienced lecturer and software veteran of Amazon, Google, Palo Alto Networks, and more! See you there! Additionally, if you’re interested in graphics programming, TPC's Graphics Division is looking to find a time for its first meeting of the semester! No prior graphics experience required! Graphics Division will be meeting weekly on Wednesdays, starting next week. Please use the when2meet to help select a time! https://www.when2meet.com/?28823530-WUAPh";
let msg_date = NaiveDate::from_ymd_opt(2025, 2, 3).unwrap();
let res = parse_msg(msg, &msg_date).await;
let date = NaiveDate::from_ymd_opt(2025, 2, 3).unwrap();
let ctx = MessageContext {
server_name: "TEST",
content: "@everyone It is my great pleasure to announce TPC's first meeting of the semester! Join us this Thursday, at 5 PM in OCNL 241 for an inspiring talk on careers, life, and projects by *James Krepelka*, an experienced lecturer and software veteran of Amazon, Google, Palo Alto Networks, and more! See you there! Additionally, if you’re interested in graphics programming, TPC's Graphics Division is looking to find a time for its first meeting of the semester! No prior graphics experience required! Graphics Division will be meeting weekly on Wednesdays, starting next week. Please use the when2meet to help select a time! https://www.when2meet.com/?28823530-WUAPh",
date
};
let res = parse_msg(&ctx).await;

assert!(res.is_ok());
let calendar = res.unwrap();
Expand Down Expand Up @@ -650,9 +692,13 @@ mod tests {
#[tokio::test]
#[ignore]
async fn real_tpc_11_20_24() {
let msg = "@everyone It is my great pleasure to announce TPC's first meeting of the semester! Join us this Thursday, at 5 PM in OCNL 241 for an inspiring talk on careers, life, and projects by *James Krepelka*, an experienced lecturer and software veteran of Amazon, Google, Palo Alto Networks, and more! See you there! Additionally, if you’re interested in graphics programming, TPC's Graphics Division is looking to find a time for its first meeting of the semester! No prior graphics experience required! Graphics Division will be meeting weekly on Wednesdays, starting next week. Please use the when2meet to help select a time! https://www.when2meet.com/?28823530-WUAPh";
let msg_date = NaiveDate::from_ymd_opt(2024, 11, 20).unwrap();
let res = parse_msg(msg, &msg_date).await;
let date = NaiveDate::from_ymd_opt(2024, 11, 20).unwrap();
let ctx = MessageContext {
server_name: "TEST",
content: "@everyone It is my great pleasure to announce TPC's first meeting of the semester! Join us this Thursday, at 5 PM in OCNL 241 for an inspiring talk on careers, life, and projects by *James Krepelka*, an experienced lecturer and software veteran of Amazon, Google, Palo Alto Networks, and more! See you there! Additionally, if you’re interested in graphics programming, TPC's Graphics Division is looking to find a time for its first meeting of the semester! No prior graphics experience required! Graphics Division will be meeting weekly on Wednesdays, starting next week. Please use the when2meet to help select a time! https://www.when2meet.com/?28823530-WUAPh",
date
};
let res = parse_msg(&ctx).await;

assert!(res.is_ok());
let calendar = res.unwrap();
Expand Down