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}
22
23#[derive(Serialize, Deserialize, PartialEq, Clone, ToSchema)]
24
25pub struct ChatbotConversationInfo {
27 pub current_conversation: Option<ChatbotConversation>,
28 pub current_conversation_messages: Option<Vec<ChatbotConversationMessage>>,
29 pub current_conversation_message_citations: Option<Vec<ChatbotConversationMessageCitation>>,
30 pub chatbot_name: String,
31 pub course_name: Option<String>,
32 pub hide_citations: bool,
33 pub suggested_messages: Option<Vec<ChatbotConversationSuggestedMessage>>,
38}
39
40pub async fn insert(
41 conn: &mut PgConnection,
42 input: ChatbotConversation,
43) -> ModelResult<ChatbotConversation> {
44 let res = sqlx::query_as!(
45 ChatbotConversation,
46 r#"
47INSERT INTO chatbot_conversations (
48 course_id,
49 user_id,
50 anonymous_token,
51 chatbot_configuration_id
52 )
53VALUES ($1, $2, $3, $4)
54RETURNING *
55 "#,
56 input.course_id,
57 input.user_id,
58 input.anonymous_token,
59 input.chatbot_configuration_id
60 )
61 .fetch_one(conn)
62 .await?;
63 Ok(res)
64}
65
66pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<ChatbotConversation> {
67 let res = sqlx::query_as!(
68 ChatbotConversation,
69 r#"
70SELECT *
71FROM chatbot_conversations
72WHERE id = $1
73 AND deleted_at IS NULL
74 "#,
75 id
76 )
77 .fetch_one(conn)
78 .await?;
79 Ok(res)
80}
81
82pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
90 sqlx::query!(
91 r#"
92UPDATE chatbot_conversations
93SET deleted_at = now()
94WHERE id = $1
95 AND deleted_at IS NULL
96 "#,
97 id
98 )
99 .execute(conn)
100 .await?;
101 Ok(())
102}
103
104pub async fn create_for_user_and_configuration(
105 conn: &mut PgConnection,
106 pkey_policy: PKeyPolicy<Uuid>,
107 user_id: Option<Uuid>,
108 anonymous_token: Option<String>,
109 chatbot_configuration_id: Uuid,
110) -> ModelResult<ChatbotConversation> {
111 let res = sqlx::query_as!(
112 ChatbotConversation,
113 r#"
114INSERT INTO chatbot_conversations (
115 id,
116 course_id,
117 user_id,
118 anonymous_token,
119 chatbot_configuration_id
120 )
121SELECT $1,
122 chatbot_configurations.course_id,
123 $2,
124 $3,
125 chatbot_configurations.id
126FROM chatbot_configurations
127WHERE chatbot_configurations.id = $4
128 AND chatbot_configurations.deleted_at IS NULL
129RETURNING *
130 "#,
131 pkey_policy.into_uuid(),
132 user_id,
133 anonymous_token,
134 chatbot_configuration_id
135 )
136 .fetch_one(conn)
137 .await?;
138 Ok(res)
139}
140
141pub async fn get_latest_conversation_for_user(
142 conn: &mut PgConnection,
143 user_id: Option<Uuid>,
144 anonymous_token: Option<String>,
145 chatbot_configuration_id: Uuid,
146) -> ModelResult<ChatbotConversation> {
147 if let (Some(_user_id), Some(_anonymous_token)) = (&user_id, &anonymous_token) {
148 return Err(model_err!(
149 InvalidRequest,
150 "User ID and anonymous token cannot both be present".to_string()
151 ));
152 }
153 let res = sqlx::query_as!(
154 ChatbotConversation,
155 r#"
156SELECT *
157FROM chatbot_conversations
158WHERE (
159 user_id = $1
160 OR anonymous_token = $2
161 )
162 AND chatbot_configuration_id = $3
163 AND deleted_at IS NULL
164ORDER BY created_at DESC
165LIMIT 1
166 "#,
167 user_id,
168 anonymous_token,
169 chatbot_configuration_id
170 )
171 .fetch_one(conn)
172 .await?;
173 Ok(res)
174}
175
176pub async fn get_current_conversation_info(
178 tx: &mut PgConnection,
179 user_id: Option<Uuid>,
180 anonymous_token: Option<String>,
181 chatbot_configuration_id: Uuid,
182) -> ModelResult<ChatbotConversationInfo> {
183 let chatbot_configuration =
184 crate::chatbot_configurations::get_by_id(tx, chatbot_configuration_id).await?;
185 let course = if let Some(course_id) = chatbot_configuration.course_id {
186 Some(crate::courses::get_course(tx, course_id).await?)
187 } else {
188 None
189 };
190
191 let current_conversation =
192 get_latest_conversation_for_user(tx, user_id, anonymous_token, chatbot_configuration_id)
193 .await
194 .optional()?;
195 let current_conversation_id = current_conversation.as_ref().map(|c| c.id);
196 let current_conversation_messages = OptionFuture::from(current_conversation_id.map(|id| {
198 crate::chatbot_conversation_messages::get_by_conversation_id_for_display(tx, id)
199 }))
200 .await
201 .transpose()?;
202
203 let current_conversation_message_citations =
204 OptionFuture::from(current_conversation_id.map(|id| {
205 crate::chatbot_conversation_messages_citations::get_by_conversation_id(tx, id)
206 }))
207 .await
208 .transpose()?;
209
210 let suggested_messages = if chatbot_configuration.suggest_next_messages
211 && let Some(ccm) = ¤t_conversation_messages
212 && !crate::chatbot_conversation_messages::turn_is_suspended(ccm)
215 && let Some(last_ccm) = ccm.last()
216 {
217 let sm = crate::chatbot_conversation_suggested_messages::get_by_conversation_message_id(
218 tx,
219 last_ccm.id.to_owned(),
220 )
221 .await?;
222 Some(sm)
224 } else {
225 None
226 };
227
228 Ok(ChatbotConversationInfo {
229 current_conversation,
230 current_conversation_messages,
231 current_conversation_message_citations,
232 suggested_messages,
233 chatbot_name: chatbot_configuration.chatbot_name,
235 course_name: course.map(|course| course.name),
236 hide_citations: chatbot_configuration.hide_citations,
237 })
238}