Skip to main content

headless_lms_models/
chatbot_configurations.rs

1use crate::prelude::*;
2use utoipa::ToSchema;
3
4#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
5#[sqlx(type_name = "reasoning_effort_level", rename_all = "snake_case")]
6#[serde(rename_all = "snake_case")]
7pub enum ReasoningEffortLevel {
8    None,
9    Minimal,
10    Low,
11    Medium,
12    High,
13    Xhigh,
14}
15
16#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
17#[sqlx(type_name = "verbosity_level", rename_all = "snake_case")]
18#[serde(rename_all = "snake_case")]
19pub enum VerbosityLevel {
20    Low,
21    Medium,
22    High,
23}
24
25#[derive(Clone, PartialEq, Deserialize, Serialize, ToSchema)]
26pub struct ChatbotConfiguration {
27    pub id: Uuid,
28    pub created_at: DateTime<Utc>,
29    pub updated_at: DateTime<Utc>,
30    pub deleted_at: Option<DateTime<Utc>>,
31    pub course_id: Option<Uuid>,
32    pub enabled_to_students: bool,
33    pub chatbot_name: String,
34    pub model_id: Uuid,
35    pub prompt: String,
36    pub initial_message: String,
37    pub weekly_tokens_per_user: i32,
38    pub daily_tokens_per_user: i32,
39    pub temperature: f32,
40    pub top_p: f32,
41    pub frequency_penalty: f32,
42    pub presence_penalty: f32,
43    pub max_output_tokens: i32,
44    pub verbosity: VerbosityLevel,
45    pub reasoning_effort: ReasoningEffortLevel,
46    pub use_azure_search: bool,
47    pub maintain_azure_search_index: bool,
48    pub hide_citations: bool,
49    pub use_semantic_reranking: bool,
50    pub use_tools: bool,
51    pub default_chatbot: bool,
52    pub suggest_next_messages: bool,
53    pub initial_suggested_messages: Option<Vec<String>>,
54}
55
56impl Default for ChatbotConfiguration {
57    fn default() -> Self {
58        Self {
59            id: Uuid::nil(),
60            created_at: Default::default(),
61            updated_at: Default::default(),
62            deleted_at: None,
63            course_id: Default::default(),
64            enabled_to_students: false,
65            chatbot_name: Default::default(),
66            model_id: Uuid::nil(),
67            prompt: Default::default(),
68            initial_message: Default::default(),
69            weekly_tokens_per_user: 20000 * 5,
70            daily_tokens_per_user: 20000,
71            max_output_tokens: 20_000,
72            temperature: 0.7,
73            top_p: 1.0,
74            frequency_penalty: 0.0,
75            presence_penalty: 0.0,
76            reasoning_effort: ReasoningEffortLevel::Minimal,
77            verbosity: VerbosityLevel::Medium,
78            use_azure_search: false,
79            maintain_azure_search_index: false,
80            hide_citations: false,
81            use_semantic_reranking: false,
82            use_tools: false,
83            default_chatbot: false,
84            suggest_next_messages: false,
85            initial_suggested_messages: None,
86        }
87    }
88}
89
90#[derive(Clone, PartialEq, Deserialize, Serialize, Debug, ToSchema)]
91
92pub struct NewChatbotConf {
93    pub course_id: Option<Uuid>,
94    pub enabled_to_students: bool,
95    pub chatbot_name: String,
96    pub model_id: Uuid,
97    pub prompt: String,
98    pub initial_message: String,
99    pub weekly_tokens_per_user: i32,
100    pub daily_tokens_per_user: i32,
101    pub temperature: f32,
102    pub top_p: f32,
103    pub frequency_penalty: f32,
104    pub presence_penalty: f32,
105    pub max_output_tokens: i32,
106    pub verbosity: VerbosityLevel,
107    pub reasoning_effort: ReasoningEffortLevel,
108    pub use_azure_search: bool,
109    pub maintain_azure_search_index: bool,
110    pub hide_citations: bool,
111    pub use_semantic_reranking: bool,
112    pub use_tools: bool,
113    pub default_chatbot: bool,
114    pub chatbotconf_id: Option<Uuid>,
115    pub suggest_next_messages: bool,
116    pub initial_suggested_messages: Option<Vec<String>>,
117}
118
119impl Default for NewChatbotConf {
120    fn default() -> Self {
121        let chatbot_conf: ChatbotConfiguration = ChatbotConfiguration::default();
122        Self {
123            course_id: chatbot_conf.course_id,
124            enabled_to_students: chatbot_conf.enabled_to_students,
125            chatbot_name: chatbot_conf.chatbot_name,
126            model_id: chatbot_conf.model_id,
127            prompt: chatbot_conf.prompt,
128            initial_message: chatbot_conf.initial_message,
129            weekly_tokens_per_user: chatbot_conf.weekly_tokens_per_user,
130            daily_tokens_per_user: chatbot_conf.daily_tokens_per_user,
131            temperature: chatbot_conf.temperature,
132            top_p: chatbot_conf.top_p,
133            frequency_penalty: chatbot_conf.frequency_penalty,
134            presence_penalty: chatbot_conf.presence_penalty,
135            max_output_tokens: chatbot_conf.max_output_tokens,
136            verbosity: chatbot_conf.verbosity,
137            reasoning_effort: chatbot_conf.reasoning_effort,
138            use_azure_search: chatbot_conf.use_azure_search,
139            maintain_azure_search_index: chatbot_conf.maintain_azure_search_index,
140            hide_citations: chatbot_conf.hide_citations,
141            use_semantic_reranking: chatbot_conf.use_semantic_reranking,
142            use_tools: chatbot_conf.use_tools,
143            default_chatbot: chatbot_conf.default_chatbot,
144            chatbotconf_id: None,
145            suggest_next_messages: chatbot_conf.suggest_next_messages,
146            initial_suggested_messages: chatbot_conf.initial_suggested_messages,
147        }
148    }
149}
150
151/// Minimum `max_output_tokens` allowed for a configuration. Too small a budget cannot produce a
152/// usable response — and with reasoning models the hidden reasoning tokens are spent from the same
153/// budget, so the floor needs to leave room for the actual answer either way.
154const MIN_MAX_OUTPUT_TOKENS: i32 = 10_000;
155
156/// Rejects configurations whose `max_output_tokens` is too small to produce a usable response.
157fn validate_max_output_tokens(input: &NewChatbotConf) -> ModelResult<()> {
158    if input.max_output_tokens < MIN_MAX_OUTPUT_TOKENS {
159        return Err(model_err!(
160            PreconditionFailed,
161            format!("max_output_tokens must be at least {MIN_MAX_OUTPUT_TOKENS}.")
162        ));
163    }
164    Ok(())
165}
166
167pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<ChatbotConfiguration> {
168    let res = sqlx::query_as!(
169        ChatbotConfiguration,
170        r#"
171SELECT *
172FROM chatbot_configurations
173WHERE id = $1
174AND deleted_at IS NULL
175        "#,
176        id
177    )
178    .fetch_one(conn)
179    .await?;
180    Ok(res)
181}
182
183pub async fn insert(
184    conn: &mut PgConnection,
185    pkey_policy: PKeyPolicy<Uuid>,
186    input: NewChatbotConf,
187) -> ModelResult<ChatbotConfiguration> {
188    validate_max_output_tokens(&input)?;
189    let maintain_azure_search_index = input.use_azure_search;
190    let res = sqlx::query_as!(
191        ChatbotConfiguration,
192        r#"
193INSERT INTO chatbot_configurations (
194    id,
195    course_id,
196    enabled_to_students,
197    chatbot_name,
198    model_id,
199    prompt,
200    initial_message,
201    weekly_tokens_per_user,
202    daily_tokens_per_user,
203    temperature,
204    top_p,
205    hide_citations,
206    frequency_penalty,
207    presence_penalty,
208    max_output_tokens,
209    verbosity,
210    reasoning_effort,
211    use_azure_search,
212    use_tools,
213    maintain_azure_search_index,
214    default_chatbot,
215    suggest_next_messages,
216    initial_suggested_messages
217  )
218VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
219RETURNING *
220        "#,
221        pkey_policy.into_uuid(),
222        input.course_id,
223        input.enabled_to_students,
224        input.chatbot_name,
225        input.model_id,
226        input.prompt,
227        input.initial_message,
228        input.weekly_tokens_per_user,
229        input.daily_tokens_per_user,
230        input.temperature,
231        input.top_p,
232        input.hide_citations,
233        input.frequency_penalty,
234        input.presence_penalty,
235        input.max_output_tokens,
236        input.verbosity as VerbosityLevel,
237        input.reasoning_effort as ReasoningEffortLevel,
238        input.use_azure_search,
239        input.use_tools,
240        maintain_azure_search_index,
241        input.default_chatbot,
242        input.suggest_next_messages,
243        input.initial_suggested_messages.as_deref(),
244    )
245    .fetch_one(conn)
246    .await?;
247    Ok(res)
248}
249
250pub async fn edit(
251    conn: &mut PgConnection,
252    input: NewChatbotConf,
253    chatbot_configuration_id: Uuid,
254) -> ModelResult<ChatbotConfiguration> {
255    validate_max_output_tokens(&input)?;
256    let res = sqlx::query_as!(
257        ChatbotConfiguration,
258        r#"
259UPDATE chatbot_configurations
260SET
261    enabled_to_students = $1,
262    chatbot_name = $2,
263    prompt = $3,
264    initial_message = $4,
265    weekly_tokens_per_user = $5,
266    daily_tokens_per_user = $6,
267    temperature = $7,
268    top_p = $8,
269    frequency_penalty = $9,
270    presence_penalty = $10,
271    max_output_tokens = $11,
272    use_azure_search = $12,
273    maintain_azure_search_index = $13,
274    hide_citations = $14,
275    use_semantic_reranking = $15,
276    default_chatbot = $16,
277    model_id = $17,
278    verbosity = $18,
279    reasoning_effort = $19,
280    use_tools = $20,
281    suggest_next_messages = $21,
282    initial_suggested_messages = $22
283WHERE id = $23
284    AND deleted_at IS NULL
285RETURNING *
286"#,
287        input.enabled_to_students,
288        input.chatbot_name,
289        input.prompt,
290        input.initial_message,
291        input.weekly_tokens_per_user,
292        input.daily_tokens_per_user,
293        input.temperature,
294        input.top_p,
295        input.frequency_penalty,
296        input.presence_penalty,
297        input.max_output_tokens,
298        input.use_azure_search,
299        input.maintain_azure_search_index,
300        input.hide_citations,
301        input.use_semantic_reranking,
302        input.default_chatbot,
303        input.model_id,
304        input.verbosity as VerbosityLevel,
305        input.reasoning_effort as ReasoningEffortLevel,
306        input.use_tools,
307        input.suggest_next_messages,
308        input.initial_suggested_messages.as_deref(),
309        chatbot_configuration_id,
310    )
311    .fetch_one(conn)
312    .await?;
313    Ok(res)
314}
315
316pub async fn delete(conn: &mut PgConnection, chatbot_configuration_id: Uuid) -> ModelResult<()> {
317    sqlx::query!(
318        r#"
319UPDATE chatbot_configurations
320SET deleted_at = now()
321WHERE id = $1
322AND deleted_at IS NULL
323        "#,
324        chatbot_configuration_id
325    )
326    .execute(conn)
327    .await?;
328    Ok(())
329}
330
331pub async fn get_for_course(
332    conn: &mut PgConnection,
333    course_id: Uuid,
334) -> ModelResult<Vec<ChatbotConfiguration>> {
335    let res = sqlx::query_as!(
336        ChatbotConfiguration,
337        r#"
338SELECT *
339FROM chatbot_configurations
340WHERE course_id = $1
341AND deleted_at IS NULL
342"#,
343        course_id
344    )
345    .fetch_all(conn)
346    .await?;
347    Ok(res)
348}
349
350pub async fn get_enabled_nondefault_for_course(
351    conn: &mut PgConnection,
352    course_id: Uuid,
353) -> ModelResult<Vec<ChatbotConfiguration>> {
354    let res = sqlx::query_as!(
355        ChatbotConfiguration,
356        r#"
357SELECT *
358FROM chatbot_configurations
359WHERE course_id = $1
360AND default_chatbot IS false
361AND enabled_to_students IS true
362AND deleted_at IS NULL
363"#,
364        course_id
365    )
366    .fetch_all(conn)
367    .await?;
368    Ok(res)
369}
370
371pub async fn get_for_azure_search_maintenance(
372    conn: &mut PgConnection,
373) -> ModelResult<Vec<ChatbotConfiguration>> {
374    let res = sqlx::query_as!(
375        ChatbotConfiguration,
376        r#"
377SELECT *
378FROM chatbot_configurations
379WHERE maintain_azure_search_index = true
380AND deleted_at IS NULL
381"#,
382    )
383    .fetch_all(conn)
384    .await?;
385    Ok(res)
386}
387
388pub async fn remove_default_chatbot_from_course(
389    conn: &mut PgConnection,
390    course_id: Uuid,
391) -> ModelResult<()> {
392    sqlx::query!(
393        r#"
394UPDATE chatbot_configurations
395SET default_chatbot = false
396WHERE course_id = $1
397AND default_chatbot = true
398AND deleted_at IS NULL
399"#,
400        course_id,
401    )
402    .execute(conn)
403    .await?;
404    Ok(())
405}
406
407pub async fn set_default_chatbot_for_course(
408    conn: &mut PgConnection,
409    chatbot_configuration_id: Uuid,
410) -> ModelResult<ChatbotConfiguration> {
411    let res = sqlx::query_as!(
412        ChatbotConfiguration,
413        r#"
414UPDATE chatbot_configurations
415SET default_chatbot = TRUE
416WHERE id = $1
417  AND deleted_at IS NULL
418RETURNING *
419"#,
420        chatbot_configuration_id,
421    )
422    .fetch_one(conn)
423    .await?;
424    Ok(res)
425}
426
427pub async fn get_all_chatbots(conn: &mut PgConnection) -> ModelResult<Vec<ChatbotConfiguration>> {
428    let res = sqlx::query_as!(
429        ChatbotConfiguration,
430        r#"
431    SELECT *
432    FROM chatbot_configurations
433    WHERE deleted_at IS NULL
434    "#,
435    )
436    .fetch_all(conn)
437    .await?;
438    Ok(res)
439}