1use futures::future::OptionFuture;
2use utoipa::ToSchema;
3
4use crate::{
5 chatbot_conversation_messages::ChatbotConversationMessage,
6 chatbot_conversation_messages_citations::ChatbotConversationMessageCitation,
7 chatbot_conversation_suggested_messages::ChatbotConversationSuggestedMessage, prelude::*,
8};
9
10#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
11
12pub struct ChatbotConversation {
13 pub id: Uuid,
14 pub anonymous_token: Option<String>,
15 pub created_at: DateTime<Utc>,
16 pub updated_at: DateTime<Utc>,
17 pub deleted_at: Option<DateTime<Utc>>,
18 pub course_id: Option<Uuid>,
19 pub user_id: Option<Uuid>,
20 pub chatbot_configuration_id: Uuid,
21 pub conversation_title: Option<String>,
22}
23
24#[derive(Serialize, Deserialize, PartialEq, Clone, ToSchema)]
25
26pub struct ChatbotConversationInfo {
28 pub current_conversation: Option<ChatbotConversation>,
29 pub current_conversation_messages: Option<Vec<ChatbotConversationMessage>>,
30 pub current_conversation_message_citations: Option<Vec<ChatbotConversationMessageCitation>>,
31 pub chatbot_name: String,
32 pub course_name: Option<String>,
33 pub hide_citations: bool,
34 pub suggested_messages: Option<Vec<ChatbotConversationSuggestedMessage>>,
39}
40
41pub async fn insert(
42 conn: &mut PgConnection,
43 input: ChatbotConversation,
44) -> ModelResult<ChatbotConversation> {
45 let res = sqlx::query_as!(
46 ChatbotConversation,
47 r#"
48INSERT INTO chatbot_conversations (
49 course_id,
50 user_id,
51 anonymous_token,
52 chatbot_configuration_id,
53 conversation_title
54 )
55VALUES ($1, $2, $3, $4, $5)
56RETURNING *
57 "#,
58 input.course_id,
59 input.user_id,
60 input.anonymous_token,
61 input.chatbot_configuration_id,
62 input.conversation_title
63 )
64 .fetch_one(conn)
65 .await?;
66 Ok(res)
67}
68
69pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<ChatbotConversation> {
70 let res = sqlx::query_as!(
71 ChatbotConversation,
72 r#"
73SELECT *
74FROM chatbot_conversations
75WHERE id = $1
76 AND deleted_at IS NULL
77 "#,
78 id
79 )
80 .fetch_one(conn)
81 .await?;
82 Ok(res)
83}
84
85pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
93 sqlx::query!(
94 r#"
95UPDATE chatbot_conversations
96SET deleted_at = now()
97WHERE id = $1
98 AND deleted_at IS NULL
99 "#,
100 id
101 )
102 .execute(conn)
103 .await?;
104 Ok(())
105}
106
107pub async fn create_for_user_and_configuration(
108 conn: &mut PgConnection,
109 pkey_policy: PKeyPolicy<Uuid>,
110 user_id: Option<Uuid>,
111 anonymous_token: Option<String>,
112 chatbot_configuration_id: Uuid,
113) -> ModelResult<ChatbotConversation> {
114 let res = sqlx::query_as!(
115 ChatbotConversation,
116 r#"
117INSERT INTO chatbot_conversations (
118 id,
119 course_id,
120 user_id,
121 anonymous_token,
122 chatbot_configuration_id,
123 conversation_title
124 )
125SELECT $1,
126 chatbot_configurations.course_id,
127 $2,
128 $3,
129 chatbot_configurations.id,
130 NULL
131FROM chatbot_configurations
132WHERE chatbot_configurations.id = $4
133 AND chatbot_configurations.deleted_at IS NULL
134RETURNING *
135 "#,
136 pkey_policy.into_uuid(),
137 user_id,
138 anonymous_token,
139 chatbot_configuration_id,
140 )
141 .fetch_one(conn)
142 .await?;
143 Ok(res)
144}
145
146pub async fn get_latest_conversation_for_user(
147 conn: &mut PgConnection,
148 user_id: Option<Uuid>,
149 anonymous_token: Option<String>,
150 chatbot_configuration_id: Uuid,
151) -> ModelResult<ChatbotConversation> {
152 if let (Some(_user_id), Some(_anonymous_token)) = (&user_id, &anonymous_token) {
153 return Err(model_err!(
154 InvalidRequest,
155 "User ID and anonymous token cannot both be present".to_string()
156 ));
157 }
158 let res = sqlx::query_as!(
159 ChatbotConversation,
160 r#"
161SELECT *
162FROM chatbot_conversations
163WHERE (
164 user_id = $1
165 OR anonymous_token = $2
166 )
167 AND chatbot_configuration_id = $3
168 AND deleted_at IS NULL
169ORDER BY created_at DESC
170LIMIT 1
171 "#,
172 user_id,
173 anonymous_token,
174 chatbot_configuration_id
175 )
176 .fetch_one(conn)
177 .await?;
178 Ok(res)
179}
180
181pub async fn get_conversation_for_user(
182 conn: &mut PgConnection,
183 user_id: Option<Uuid>,
184 anonymous_token: Option<String>,
185 chatbot_configuration_id: Uuid,
186 conversation_id: Uuid,
187) -> ModelResult<ChatbotConversation> {
188 let res = sqlx::query_as!(
189 ChatbotConversation,
190 r#"
191SELECT *
192FROM chatbot_conversations
193WHERE (
194 user_id = $1
195 OR anonymous_token = $2
196 )
197 AND chatbot_configuration_id = $3
198 AND id = $4
199 AND deleted_at IS NULL
200 "#,
201 user_id,
202 anonymous_token,
203 chatbot_configuration_id,
204 conversation_id
205 )
206 .fetch_one(conn)
207 .await?;
208 Ok(res)
209}
210
211pub async fn get_all_conversations_for_user(
212 conn: &mut PgConnection,
213 user_id: Uuid,
214) -> ModelResult<Vec<ChatbotConversation>> {
215 let res = sqlx::query_as!(
216 ChatbotConversation,
217 r#"
218SELECT *
219FROM chatbot_conversations
220WHERE user_id = $1
221 AND deleted_at IS NULL
222ORDER BY created_at DESC
223 "#,
224 user_id,
225 )
226 .fetch_all(conn)
227 .await?;
228 Ok(res)
229}
230
231pub async fn update_conversation_title(
232 conn: &mut PgConnection,
233 conversation_id: Uuid,
234 conversation_title: String,
235) -> ModelResult<()> {
236 sqlx::query!(
237 r#"
238UPDATE chatbot_conversations
239SET conversation_title = $1
240WHERE id = $2
241 AND deleted_at IS NULL
242 "#,
243 conversation_title,
244 conversation_id
245 )
246 .execute(conn)
247 .await?;
248 Ok(())
249}
250
251pub async fn get_conversation_info(
253 tx: &mut PgConnection,
254 user_id: Option<Uuid>,
255 anonymous_token: Option<String>,
256 chatbot_configuration_id: Uuid,
257 conversation_id: Option<Uuid>,
258) -> ModelResult<ChatbotConversationInfo> {
259 let chatbot_configuration =
260 crate::chatbot_configurations::get_by_id(tx, chatbot_configuration_id).await?;
261 let course = if let Some(course_id) = chatbot_configuration.course_id {
262 Some(crate::courses::get_course(tx, course_id).await?)
263 } else {
264 None
265 };
266
267 let current_conversation = if let Some(conversation_id) = conversation_id {
268 get_conversation_for_user(
269 tx,
270 user_id,
271 anonymous_token,
272 chatbot_configuration_id,
273 conversation_id,
274 )
275 .await
276 .optional()?
277 } else {
278 get_latest_conversation_for_user(tx, user_id, anonymous_token, chatbot_configuration_id)
279 .await
280 .optional()?
281 };
282
283 let conversation_id = current_conversation.as_ref().map(|c| c.id);
284
285 let current_conversation_messages = OptionFuture::from(conversation_id.map(|id| {
286 crate::chatbot_conversation_messages::get_by_conversation_id_for_display(tx, id)
287 }))
288 .await
289 .transpose()?;
290
291 let current_conversation_message_citations =
292 OptionFuture::from(conversation_id.map(|id| {
293 crate::chatbot_conversation_messages_citations::get_by_conversation_id(tx, id)
294 }))
295 .await
296 .transpose()?;
297
298 let suggested_messages = if chatbot_configuration.suggest_next_messages
299 && let Some(ccm) = ¤t_conversation_messages
300 && !crate::chatbot_conversation_messages::turn_is_suspended(ccm)
303 && let Some(last_ccm) = ccm.last()
304 {
305 let sm = crate::chatbot_conversation_suggested_messages::get_by_conversation_message_id(
306 tx,
307 last_ccm.id.to_owned(),
308 )
309 .await?;
310 Some(sm)
312 } else {
313 None
314 };
315
316 Ok(ChatbotConversationInfo {
317 current_conversation,
318 current_conversation_messages,
319 current_conversation_message_citations,
320 suggested_messages,
321 chatbot_name: chatbot_configuration.chatbot_name,
323 course_name: course.map(|course| course.name),
324 hide_citations: chatbot_configuration.hide_citations,
325 })
326}