Skip to main content

headless_lms_models/
chatbot_conversations.rs

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 created_at: DateTime<Utc>,
15    pub updated_at: DateTime<Utc>,
16    pub deleted_at: Option<DateTime<Utc>>,
17    pub course_id: Uuid,
18    pub user_id: Uuid,
19    pub chatbot_configuration_id: Uuid,
20}
21
22#[derive(Serialize, Deserialize, PartialEq, Clone, ToSchema)]
23
24/** Should contain all information required to display the chatbot to the user. */
25pub struct ChatbotConversationInfo {
26    pub current_conversation: Option<ChatbotConversation>,
27    pub current_conversation_messages: Option<Vec<ChatbotConversationMessage>>,
28    pub current_conversation_message_citations: Option<Vec<ChatbotConversationMessageCitation>>,
29    pub chatbot_name: String,
30    pub course_name: String,
31    pub hide_citations: bool,
32    pub suggested_messages: Option<Vec<ChatbotConversationSuggestedMessage>>,
33}
34
35pub async fn insert(
36    conn: &mut PgConnection,
37    input: ChatbotConversation,
38) -> ModelResult<ChatbotConversation> {
39    let res = sqlx::query_as!(
40        ChatbotConversation,
41        r#"
42INSERT INTO chatbot_conversations (course_id, user_id, chatbot_configuration_id)
43VALUES ($1, $2, $3)
44RETURNING *
45        "#,
46        input.course_id,
47        input.user_id,
48        input.chatbot_configuration_id
49    )
50    .fetch_one(conn)
51    .await?;
52    Ok(res)
53}
54
55pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<ChatbotConversation> {
56    let res = sqlx::query_as!(
57        ChatbotConversation,
58        r#"
59SELECT *
60FROM chatbot_conversations
61WHERE id = $1
62  AND deleted_at IS NULL
63        "#,
64        id
65    )
66    .fetch_one(conn)
67    .await?;
68    Ok(res)
69}
70
71pub async fn create_for_user_and_configuration(
72    conn: &mut PgConnection,
73    pkey_policy: PKeyPolicy<Uuid>,
74    user_id: Uuid,
75    chatbot_configuration_id: Uuid,
76) -> ModelResult<ChatbotConversation> {
77    let res = sqlx::query_as!(
78        ChatbotConversation,
79        r#"
80INSERT INTO chatbot_conversations (
81    id,
82    course_id,
83    user_id,
84    chatbot_configuration_id
85)
86SELECT
87    $1,
88    chatbot_configurations.course_id,
89    $2,
90    chatbot_configurations.id
91FROM chatbot_configurations
92WHERE chatbot_configurations.id = $3
93  AND chatbot_configurations.deleted_at IS NULL
94RETURNING *
95        "#,
96        pkey_policy.into_uuid(),
97        user_id,
98        chatbot_configuration_id
99    )
100    .fetch_one(conn)
101    .await?;
102    Ok(res)
103}
104
105pub async fn get_latest_conversation_for_user(
106    conn: &mut PgConnection,
107    user_id: Uuid,
108    chatbot_configuration_id: Uuid,
109) -> ModelResult<ChatbotConversation> {
110    let res = sqlx::query_as!(
111        ChatbotConversation,
112        r#"
113SELECT *
114FROM chatbot_conversations
115WHERE user_id = $1
116  AND chatbot_configuration_id = $2
117  AND deleted_at IS NULL
118ORDER BY created_at DESC
119LIMIT 1
120        "#,
121        user_id,
122        chatbot_configuration_id
123    )
124    .fetch_one(conn)
125    .await?;
126    Ok(res)
127}
128
129/// Gets the current conversation for the user, if any. Also inlcudes information about the chatbot so that the chatbot ui can be rendered using the information.
130pub async fn get_current_conversation_info(
131    tx: &mut PgConnection,
132    user_id: Uuid,
133    chatbot_configuration_id: Uuid,
134) -> ModelResult<ChatbotConversationInfo> {
135    let chatbot_configuration =
136        crate::chatbot_configurations::get_by_id(tx, chatbot_configuration_id).await?;
137    let course = crate::courses::get_course(tx, chatbot_configuration.course_id).await?;
138    let current_conversation =
139        get_latest_conversation_for_user(tx, user_id, chatbot_configuration_id)
140            .await
141            .optional()?;
142    // the messages are sorted by response_order_number
143    let current_conversation_messages = OptionFuture::from(
144        current_conversation
145            .clone()
146            .map(|c| crate::chatbot_conversation_messages::get_by_conversation_id(tx, c.id)),
147    )
148    .await
149    .transpose()?;
150
151    let current_conversation_message_citations =
152        OptionFuture::from(current_conversation.clone().map(|c| {
153            crate::chatbot_conversation_messages_citations::get_by_conversation_id(tx, c.id)
154        }))
155        .await
156        .transpose()?;
157
158    let suggested_messages = if chatbot_configuration.suggest_next_messages
159        && let Some(ccm) = &current_conversation_messages
160        && let Some(last_ccm) = ccm.last()
161    {
162        let sm = crate::chatbot_conversation_suggested_messages::get_by_conversation_message_id(
163            tx,
164            last_ccm.id.to_owned(),
165        )
166        .await?;
167        // return an empty vec if there are not yet any suggested messages
168        Some(sm)
169    } else {
170        None
171    };
172
173    Ok(ChatbotConversationInfo {
174        current_conversation,
175        current_conversation_messages,
176        current_conversation_message_citations,
177        suggested_messages,
178        // Don't want to expose everything from the chatbot configuration to the user because it contains private information like the prompt.
179        chatbot_name: chatbot_configuration.chatbot_name,
180        course_name: course.name,
181        hide_citations: chatbot_configuration.hide_citations,
182    })
183}