Skip to main content

headless_lms_models/
chatbot_conversation_message_reasoning.rs

1use crate::prelude::*;
2use utoipa::ToSchema;
3
4#[derive(Clone, PartialEq, Deserialize, Serialize, Debug, ToSchema)]
5pub struct ChatbotConversationMessageReasoning {
6    pub id: Uuid,
7    pub chatbot_conversation_message_id: Uuid,
8    pub created_at: DateTime<Utc>,
9    pub updated_at: DateTime<Utc>,
10    pub deleted_at: Option<DateTime<Utc>>,
11    pub summary: Option<String>,
12    pub reasoning_id: String,
13    pub response_id: String,
14    /// Never serialized: this reaches the browser as part of an untagged [`Message`], and the
15    /// payload is the model's own reasoning.
16    ///
17    /// [`Message`]: crate::chatbot_conversation_messages::Message
18    #[serde(skip)]
19    pub encrypted_content: Option<String>,
20}
21
22impl Default for ChatbotConversationMessageReasoning {
23    fn default() -> Self {
24        Self {
25            id: Uuid::nil(),
26            chatbot_conversation_message_id: Uuid::nil(),
27            created_at: Default::default(),
28            updated_at: Default::default(),
29            deleted_at: None,
30            summary: None,
31            reasoning_id: Default::default(),
32            response_id: Default::default(),
33            encrypted_content: None,
34        }
35    }
36}
37
38pub async fn insert(
39    conn: &mut PgConnection,
40    input: ChatbotConversationMessageReasoning,
41    msg_id: Uuid,
42) -> ModelResult<ChatbotConversationMessageReasoning> {
43    let res = sqlx::query_as!(
44        ChatbotConversationMessageReasoning,
45        r#"
46INSERT INTO chatbot_conversation_message_reasoning (
47    chatbot_conversation_message_id,
48    summary,
49    response_id,
50    reasoning_id,
51    encrypted_content
52  )
53VALUES ($1, $2, $3, $4, $5)
54RETURNING *
55        "#,
56        msg_id,
57        input.summary,
58        input.response_id,
59        input.reasoning_id,
60        input.encrypted_content,
61    )
62    .fetch_one(conn)
63    .await?;
64    Ok(res)
65}
66
67pub async fn get_by_id(
68    conn: &mut PgConnection,
69    id: Uuid,
70) -> ModelResult<ChatbotConversationMessageReasoning> {
71    let res = sqlx::query_as!(
72        ChatbotConversationMessageReasoning,
73        r#"
74SELECT *
75FROM chatbot_conversation_message_reasoning
76WHERE id = $1
77  AND deleted_at IS NULL
78        "#,
79        id
80    )
81    .fetch_one(conn)
82    .await?;
83    Ok(res)
84}
85
86pub async fn get_by_message_id(
87    conn: &mut PgConnection,
88    message_id: Uuid,
89) -> ModelResult<Option<ChatbotConversationMessageReasoning>> {
90    let res = sqlx::query_as!(
91        ChatbotConversationMessageReasoning,
92        r#"
93SELECT *
94FROM chatbot_conversation_message_reasoning
95WHERE chatbot_conversation_message_id = $1
96  AND deleted_at IS NULL
97        "#,
98        message_id
99    )
100    .fetch_optional(conn)
101    .await?;
102    Ok(res)
103}
104
105pub async fn delete(
106    conn: &mut PgConnection,
107    id: Uuid,
108) -> ModelResult<ChatbotConversationMessageReasoning> {
109    let res = sqlx::query_as!(
110        ChatbotConversationMessageReasoning,
111        r#"
112UPDATE chatbot_conversation_message_reasoning
113SET deleted_at = NOW()
114WHERE id = $1
115  AND deleted_at IS NULL
116RETURNING *
117        "#,
118        id
119    )
120    .fetch_one(conn)
121    .await?;
122    Ok(res)
123}
124
125#[cfg(test)]
126mod tests {
127    use crate::chatbot_conversation_messages::{ChatbotConversationMessage, Message};
128
129    use super::*;
130
131    /// A conversation's messages are served to the browser as they are stored, so nothing but the
132    /// `skip` keeps the model's own reasoning out of the response. Dropping the attribute is a
133    /// one-line change that leaks every conversation's reasoning to whoever is reading it.
134    #[test]
135    fn a_reasoning_message_never_serializes_its_payload() {
136        let message = ChatbotConversationMessage {
137            message: Message::Reasoning(ChatbotConversationMessageReasoning {
138                summary: Some("Thinking about loops".to_string()),
139                encrypted_content: Some("the-opaque-payload".to_string()),
140                ..Default::default()
141            }),
142            ..Default::default()
143        };
144
145        let json = serde_json::to_string(&message).expect("the message serializes");
146
147        assert!(json.contains("Thinking about loops"), "{json}");
148        assert!(!json.contains("encrypted_content"), "{json}");
149        assert!(!json.contains("the-opaque-payload"), "{json}");
150    }
151}