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(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, ToSchema)]
28#[serde(rename_all = "snake_case")]
29pub enum ToolCategoryGroup {
30 CourseAssistance,
31 CourseDiscovery,
32 Interaction,
33 AdminSupport,
34}
35
36#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
40#[sqlx(type_name = "chatbot_tool_category", rename_all = "snake_case")]
41#[serde(rename_all = "snake_case")]
42pub enum ToolCategory {
43 CourseMaterial,
44 CourseInfo,
45 CourseCatalog,
46 Interaction,
47 AdminSupportAccounts,
48 AdminSupportCourses,
49 AdminSupportLearningProgress,
50 AdminSupportAcademicIntegrity,
51}
52
53impl ToolCategory {
54 pub const ALL: [ToolCategory; 8] = [
57 ToolCategory::CourseMaterial,
58 ToolCategory::CourseInfo,
59 ToolCategory::CourseCatalog,
60 ToolCategory::Interaction,
61 ToolCategory::AdminSupportAccounts,
62 ToolCategory::AdminSupportCourses,
63 ToolCategory::AdminSupportLearningProgress,
64 ToolCategory::AdminSupportAcademicIntegrity,
65 ];
66
67 pub const fn group(self) -> ToolCategoryGroup {
68 match self {
69 ToolCategory::CourseMaterial | ToolCategory::CourseInfo => {
70 ToolCategoryGroup::CourseAssistance
71 }
72 ToolCategory::CourseCatalog => ToolCategoryGroup::CourseDiscovery,
73 ToolCategory::Interaction => ToolCategoryGroup::Interaction,
74 ToolCategory::AdminSupportAccounts
75 | ToolCategory::AdminSupportCourses
76 | ToolCategory::AdminSupportLearningProgress
77 | ToolCategory::AdminSupportAcademicIntegrity => ToolCategoryGroup::AdminSupport,
78 }
79 }
80
81 pub const fn requires_global_admin(self) -> bool {
82 matches!(self.group(), ToolCategoryGroup::AdminSupport)
83 }
84}
85
86#[derive(Clone, PartialEq, Deserialize, Serialize, ToSchema)]
87pub struct CreateChatbotRequest {
88 pub name: String,
89 pub course_id: Option<Uuid>,
90 pub purpose: String,
91 pub skip_azure_stuff: bool,
92}
93
94#[derive(Clone, PartialEq, Deserialize, Serialize, ToSchema)]
95pub struct ChatbotConfiguration {
96 pub id: Uuid,
97 pub created_at: DateTime<Utc>,
98 pub updated_at: DateTime<Utc>,
99 pub deleted_at: Option<DateTime<Utc>>,
100 pub course_id: Option<Uuid>,
101 pub enabled_to_students: bool,
102 pub chatbot_name: String,
103 pub model_id: Uuid,
104 pub prompt: String,
105 pub initial_message: String,
106 pub weekly_tokens_per_user: i32,
107 pub daily_tokens_per_user: i32,
108 pub temperature: f32,
109 pub top_p: f32,
110 pub frequency_penalty: f32,
111 pub presence_penalty: f32,
112 pub max_output_tokens: i32,
113 pub verbosity: VerbosityLevel,
114 pub reasoning_effort: ReasoningEffortLevel,
115 pub use_azure_search: bool,
116 pub maintain_azure_search_index: bool,
117 pub hide_citations: bool,
118 pub use_semantic_reranking: bool,
119 pub enabled_tool_categories: Vec<ToolCategory>,
120 pub default_chatbot: bool,
121 pub suggest_next_messages: bool,
122 pub initial_suggested_messages: Option<Vec<String>>,
123 pub publicly_accessible: bool,
124}
125
126impl Default for ChatbotConfiguration {
127 fn default() -> Self {
128 Self {
129 id: Uuid::nil(),
130 created_at: Default::default(),
131 updated_at: Default::default(),
132 deleted_at: None,
133 course_id: Default::default(),
134 enabled_to_students: false,
135 chatbot_name: Default::default(),
136 model_id: Uuid::nil(),
137 prompt: Default::default(),
138 initial_message: Default::default(),
139 weekly_tokens_per_user: 20000 * 5,
140 daily_tokens_per_user: 20000,
141 max_output_tokens: 20_000,
142 temperature: 0.7,
143 top_p: 1.0,
144 frequency_penalty: 0.0,
145 presence_penalty: 0.0,
146 reasoning_effort: ReasoningEffortLevel::Medium,
147 verbosity: VerbosityLevel::Medium,
148 use_azure_search: true,
149 maintain_azure_search_index: true,
150 hide_citations: false,
151 use_semantic_reranking: false,
152 enabled_tool_categories: vec![
153 ToolCategory::CourseMaterial,
154 ToolCategory::CourseInfo,
155 ToolCategory::CourseCatalog,
156 ToolCategory::Interaction,
157 ],
158 default_chatbot: false,
159 suggest_next_messages: true,
160 initial_suggested_messages: None,
161 publicly_accessible: false,
162 }
163 }
164}
165
166#[derive(Clone, PartialEq, Deserialize, Serialize, Debug, ToSchema)]
167
168pub struct NewChatbotConf {
169 pub course_id: Option<Uuid>,
170 pub enabled_to_students: bool,
171 pub chatbot_name: String,
172 pub model_id: Uuid,
173 pub prompt: String,
174 pub initial_message: String,
175 pub weekly_tokens_per_user: i32,
176 pub daily_tokens_per_user: i32,
177 pub temperature: f32,
178 pub top_p: f32,
179 pub frequency_penalty: f32,
180 pub presence_penalty: f32,
181 pub max_output_tokens: i32,
182 pub verbosity: VerbosityLevel,
183 pub reasoning_effort: ReasoningEffortLevel,
184 pub use_azure_search: bool,
185 pub maintain_azure_search_index: bool,
186 pub hide_citations: bool,
187 pub use_semantic_reranking: bool,
188 pub enabled_tool_categories: Vec<ToolCategory>,
189 pub default_chatbot: bool,
190 pub chatbotconf_id: Option<Uuid>,
191 pub suggest_next_messages: bool,
192 pub initial_suggested_messages: Option<Vec<String>>,
193 pub publicly_accessible: bool,
194}
195
196impl Default for NewChatbotConf {
197 fn default() -> Self {
198 let chatbot_conf: ChatbotConfiguration = ChatbotConfiguration::default();
199 Self {
200 course_id: chatbot_conf.course_id,
201 enabled_to_students: chatbot_conf.enabled_to_students,
202 chatbot_name: chatbot_conf.chatbot_name,
203 model_id: chatbot_conf.model_id,
204 prompt: chatbot_conf.prompt,
205 initial_message: chatbot_conf.initial_message,
206 weekly_tokens_per_user: chatbot_conf.weekly_tokens_per_user,
207 daily_tokens_per_user: chatbot_conf.daily_tokens_per_user,
208 temperature: chatbot_conf.temperature,
209 top_p: chatbot_conf.top_p,
210 frequency_penalty: chatbot_conf.frequency_penalty,
211 presence_penalty: chatbot_conf.presence_penalty,
212 max_output_tokens: chatbot_conf.max_output_tokens,
213 verbosity: chatbot_conf.verbosity,
214 reasoning_effort: chatbot_conf.reasoning_effort,
215 use_azure_search: chatbot_conf.use_azure_search,
216 maintain_azure_search_index: chatbot_conf.maintain_azure_search_index,
217 hide_citations: chatbot_conf.hide_citations,
218 use_semantic_reranking: chatbot_conf.use_semantic_reranking,
219 enabled_tool_categories: chatbot_conf.enabled_tool_categories,
220 default_chatbot: chatbot_conf.default_chatbot,
221 chatbotconf_id: None,
222 suggest_next_messages: chatbot_conf.suggest_next_messages,
223 initial_suggested_messages: chatbot_conf.initial_suggested_messages,
224 publicly_accessible: chatbot_conf.publicly_accessible,
225 }
226 }
227}
228
229impl From<ChatbotConfiguration> for NewChatbotConf {
230 fn from(v: ChatbotConfiguration) -> Self {
231 Self {
232 course_id: v.course_id,
233 enabled_to_students: v.enabled_to_students,
234 chatbot_name: v.chatbot_name,
235 model_id: v.model_id,
236 prompt: v.prompt,
237 initial_message: v.initial_message,
238 weekly_tokens_per_user: v.weekly_tokens_per_user,
239 daily_tokens_per_user: v.daily_tokens_per_user,
240 temperature: v.temperature,
241 top_p: v.top_p,
242 frequency_penalty: v.frequency_penalty,
243 presence_penalty: v.presence_penalty,
244 max_output_tokens: v.max_output_tokens,
245 verbosity: v.verbosity,
246 reasoning_effort: v.reasoning_effort,
247 use_azure_search: v.use_azure_search,
248 maintain_azure_search_index: v.maintain_azure_search_index,
249 hide_citations: v.hide_citations,
250 use_semantic_reranking: v.use_semantic_reranking,
251 enabled_tool_categories: v.enabled_tool_categories,
252 default_chatbot: v.default_chatbot,
253 chatbotconf_id: Some(v.id),
254 suggest_next_messages: v.suggest_next_messages,
255 initial_suggested_messages: v.initial_suggested_messages,
256 publicly_accessible: v.publicly_accessible,
257 }
258 }
259}
260
261const MIN_MAX_OUTPUT_TOKENS: i32 = 10_000;
265
266fn validate_max_output_tokens(input: &NewChatbotConf) -> ModelResult<()> {
268 if input.max_output_tokens < MIN_MAX_OUTPUT_TOKENS {
269 return Err(model_err!(
270 PreconditionFailed,
271 format!("max_output_tokens must be at least {MIN_MAX_OUTPUT_TOKENS}.")
272 ));
273 }
274 Ok(())
275}
276
277fn azure_search_allowed(input: &NewChatbotConf, course_id: Option<Uuid>) -> bool {
280 input.use_azure_search && course_id.is_some()
281}
282
283pub fn normalized_tool_categories(input: &[ToolCategory]) -> Vec<ToolCategory> {
287 ToolCategory::ALL
288 .into_iter()
289 .filter(|category| input.contains(category))
290 .collect()
291}
292
293pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<ChatbotConfiguration> {
294 let res = sqlx::query_as!(
295 ChatbotConfiguration,
296 r#"
297SELECT *
298FROM chatbot_configurations
299WHERE id = $1
300AND deleted_at IS NULL
301 "#,
302 id
303 )
304 .fetch_one(conn)
305 .await?;
306 Ok(res)
307}
308
309pub async fn insert(
310 conn: &mut PgConnection,
311 pkey_policy: PKeyPolicy<Uuid>,
312 input: NewChatbotConf,
313) -> ModelResult<ChatbotConfiguration> {
314 validate_max_output_tokens(&input)?;
315 let use_azure_search = azure_search_allowed(&input, input.course_id);
316 let maintain_azure_search_index = use_azure_search;
317 let enabled_tool_categories = normalized_tool_categories(&input.enabled_tool_categories);
318 let res = sqlx::query_as!(
319 ChatbotConfiguration,
320 r#"
321INSERT INTO chatbot_configurations (
322 id,
323 course_id,
324 enabled_to_students,
325 chatbot_name,
326 model_id,
327 prompt,
328 initial_message,
329 weekly_tokens_per_user,
330 daily_tokens_per_user,
331 temperature,
332 top_p,
333 hide_citations,
334 frequency_penalty,
335 presence_penalty,
336 max_output_tokens,
337 verbosity,
338 reasoning_effort,
339 use_azure_search,
340 enabled_tool_categories,
341 maintain_azure_search_index,
342 default_chatbot,
343 suggest_next_messages,
344 initial_suggested_messages,
345 publicly_accessible
346 )
347VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)
348RETURNING *
349 "#,
350 pkey_policy.into_uuid(),
351 input.course_id,
352 input.enabled_to_students,
353 input.chatbot_name,
354 input.model_id,
355 input.prompt,
356 input.initial_message,
357 input.weekly_tokens_per_user,
358 input.daily_tokens_per_user,
359 input.temperature,
360 input.top_p,
361 input.hide_citations,
362 input.frequency_penalty,
363 input.presence_penalty,
364 input.max_output_tokens,
365 input.verbosity as VerbosityLevel,
366 input.reasoning_effort as ReasoningEffortLevel,
367 use_azure_search,
368 &enabled_tool_categories as &[ToolCategory],
369 maintain_azure_search_index,
370 input.default_chatbot,
371 input.suggest_next_messages,
372 input.initial_suggested_messages.as_deref(),
373 input.publicly_accessible
374 )
375 .fetch_one(conn)
376 .await?;
377 Ok(res)
378}
379
380pub async fn edit(
381 conn: &mut PgConnection,
382 input: NewChatbotConf,
383 chatbot_configuration_id: Uuid,
384) -> ModelResult<ChatbotConfiguration> {
385 validate_max_output_tokens(&input)?;
386 let course_id = get_by_id(conn, chatbot_configuration_id).await?.course_id;
389 let use_azure_search = azure_search_allowed(&input, course_id);
390 let enabled_tool_categories = normalized_tool_categories(&input.enabled_tool_categories);
391 let res = sqlx::query_as!(
392 ChatbotConfiguration,
393 r#"
394UPDATE chatbot_configurations
395SET
396 enabled_to_students = $1,
397 chatbot_name = $2,
398 prompt = $3,
399 initial_message = $4,
400 weekly_tokens_per_user = $5,
401 daily_tokens_per_user = $6,
402 temperature = $7,
403 top_p = $8,
404 frequency_penalty = $9,
405 presence_penalty = $10,
406 max_output_tokens = $11,
407 use_azure_search = $12,
408 maintain_azure_search_index = $13,
409 hide_citations = $14,
410 use_semantic_reranking = $15,
411 default_chatbot = $16,
412 model_id = $17,
413 verbosity = $18,
414 reasoning_effort = $19,
415 enabled_tool_categories = $20,
416 suggest_next_messages = $21,
417 initial_suggested_messages = $22,
418 publicly_accessible = $23
419WHERE id = $24
420 AND deleted_at IS NULL
421RETURNING *
422"#,
423 input.enabled_to_students,
424 input.chatbot_name,
425 input.prompt,
426 input.initial_message,
427 input.weekly_tokens_per_user,
428 input.daily_tokens_per_user,
429 input.temperature,
430 input.top_p,
431 input.frequency_penalty,
432 input.presence_penalty,
433 input.max_output_tokens,
434 use_azure_search,
435 use_azure_search,
436 input.hide_citations,
437 input.use_semantic_reranking,
438 input.default_chatbot,
439 input.model_id,
440 input.verbosity as VerbosityLevel,
441 input.reasoning_effort as ReasoningEffortLevel,
442 &enabled_tool_categories as &[ToolCategory],
443 input.suggest_next_messages,
444 input.initial_suggested_messages.as_deref(),
445 input.publicly_accessible,
446 chatbot_configuration_id,
447 )
448 .fetch_one(conn)
449 .await?;
450 Ok(res)
451}
452
453pub async fn delete(conn: &mut PgConnection, chatbot_configuration_id: Uuid) -> ModelResult<()> {
454 sqlx::query!(
455 r#"
456UPDATE chatbot_configurations
457SET deleted_at = now()
458WHERE id = $1
459AND deleted_at IS NULL
460 "#,
461 chatbot_configuration_id
462 )
463 .execute(conn)
464 .await?;
465 Ok(())
466}
467
468pub async fn get_for_course(
469 conn: &mut PgConnection,
470 course_id: Uuid,
471) -> ModelResult<Vec<ChatbotConfiguration>> {
472 let res = sqlx::query_as!(
473 ChatbotConfiguration,
474 r#"
475SELECT *
476FROM chatbot_configurations
477WHERE course_id = $1
478AND deleted_at IS NULL
479"#,
480 course_id
481 )
482 .fetch_all(conn)
483 .await?;
484 Ok(res)
485}
486
487pub async fn get_enabled_nondefault_for_course(
488 conn: &mut PgConnection,
489 course_id: Uuid,
490) -> ModelResult<Vec<ChatbotConfiguration>> {
491 let res = sqlx::query_as!(
492 ChatbotConfiguration,
493 r#"
494SELECT *
495FROM chatbot_configurations
496WHERE course_id = $1
497AND default_chatbot IS false
498AND enabled_to_students IS true
499AND deleted_at IS NULL
500"#,
501 course_id
502 )
503 .fetch_all(conn)
504 .await?;
505 Ok(res)
506}
507
508pub async fn get_for_azure_search_maintenance(
509 conn: &mut PgConnection,
510) -> ModelResult<Vec<ChatbotConfiguration>> {
511 let res = sqlx::query_as!(
512 ChatbotConfiguration,
513 r#"
514SELECT *
515FROM chatbot_configurations
516WHERE maintain_azure_search_index = true
517AND deleted_at IS NULL
518"#,
519 )
520 .fetch_all(conn)
521 .await?;
522 Ok(res)
523}
524
525pub async fn remove_default_chatbot_from_course(
526 conn: &mut PgConnection,
527 course_id: Uuid,
528) -> ModelResult<()> {
529 sqlx::query!(
530 r#"
531UPDATE chatbot_configurations
532SET default_chatbot = false
533WHERE course_id = $1
534AND default_chatbot = true
535AND deleted_at IS NULL
536"#,
537 course_id,
538 )
539 .execute(conn)
540 .await?;
541 Ok(())
542}
543
544pub async fn set_default_chatbot_for_course(
545 conn: &mut PgConnection,
546 chatbot_configuration_id: Uuid,
547) -> ModelResult<ChatbotConfiguration> {
548 let res = sqlx::query_as!(
549 ChatbotConfiguration,
550 r#"
551UPDATE chatbot_configurations
552SET default_chatbot = TRUE
553WHERE id = $1
554 AND deleted_at IS NULL
555RETURNING *
556"#,
557 chatbot_configuration_id,
558 )
559 .fetch_one(conn)
560 .await?;
561 Ok(res)
562}
563
564pub async fn get_all_chatbots(conn: &mut PgConnection) -> ModelResult<Vec<ChatbotConfiguration>> {
565 let res = sqlx::query_as!(
566 ChatbotConfiguration,
567 r#"
568 SELECT *
569 FROM chatbot_configurations
570 WHERE deleted_at IS NULL
571 "#,
572 )
573 .fetch_all(conn)
574 .await?;
575 Ok(res)
576}