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