Skip to main content

headless_lms_chatbot/chatbot_tools/custom_tools/
course_progress.rs

1use std::collections::HashMap;
2
3use crate::{
4    azure_chatbot::ChatbotUserContext,
5    chatbot_error::chatbot_err,
6    chatbot_tools::{
7        AzureLLMFunctionToolDefinition, ChatbotTool, LLMToolParamType, LLMToolParams, LLMToolType,
8        ToolProperties,
9    },
10    prelude::{ChatbotError, ChatbotErrorType, ChatbotResult},
11};
12use headless_lms_base::prelude_base_and_re_exports::BackendError;
13use headless_lms_models::{
14    course_modules::{CompletionPolicy, CourseModule},
15    user_exercise_states::UserCourseProgress,
16};
17use sqlx::PgConnection;
18
19pub type CourseProgressTool = ToolProperties<CourseProgressState, CourseProgressArguments>;
20
21impl ChatbotTool for CourseProgressTool {
22    type State = CourseProgressState;
23    type Arguments = CourseProgressArguments;
24
25    fn parse_arguments(_args_string: String) -> ChatbotResult<Self::Arguments> {
26        Ok(CourseProgressArguments {})
27    }
28
29    /// Create a CourseProgressTool instance
30    async fn from_db_and_arguments(
31        conn: &mut PgConnection,
32        arguments: Self::Arguments,
33        user_context: &ChatbotUserContext,
34    ) -> ChatbotResult<Self> {
35        let Some(user_id) = user_context.user_id else {
36            return Err(chatbot_err!(
37                ToolUseError,
38                "User id is missing.".to_string()
39            ));
40        };
41        let Some(course_id) = user_context.course_id else {
42            return Err(chatbot_err!(
43                ToolUseError,
44                "Course id is missing.".to_string()
45            ));
46        };
47        let Some(course_name) = &user_context.course_name else {
48            return Err(chatbot_err!(
49                ToolUseError,
50                "Course name is missing.".to_string()
51            ));
52        };
53        let user_progress = headless_lms_models::user_exercise_states::get_user_course_progress(
54            conn, course_id, user_id, true,
55        )
56        .await?;
57        let modules =
58            headless_lms_models::course_modules::get_by_course_id(conn, course_id).await?;
59        let progress = progress_info(user_progress, modules, course_name)?;
60        Result::Ok(CourseProgressTool {
61            state: CourseProgressState {
62                course_name: course_name.clone(),
63                progress,
64            },
65            arguments,
66        })
67    }
68
69    /// Return a string explaining the user's progress on the course that the chatbot is on
70    fn output(&self) -> String {
71        let mut progress = self.state.progress.to_owned();
72        let course_name = &self.state.course_name;
73        let mut res = format!("The user is completing a course called {course_name}. ");
74
75        // If `progress` has one value, then this course has only one (default) module
76        if progress.len() == 1 {
77            let progress_info = &progress[0];
78            let module_progress = &progress_info.progress;
79
80            res += "Their progress on this course is the following:";
81
82            res += &push_exercises_scores_progress(
83                module_progress,
84                progress_info.automatic_completion,
85                progress_info.requires_exam,
86                "course",
87            );
88        } else {
89            // If there are multiple modules in this course, then each module has its
90            // own progress
91            progress.sort_by_key(|m| m.order_number);
92            let first_mod = progress.first();
93
94            // the first in the sorted list is the base module
95            let s = if let Some(progress_info) = first_mod {
96                let module_progress = &progress_info.progress;
97                let m_name = &module_progress.course_module_name;
98                format!(
99                    "The course has one base module, and additional modules. The user's progress on the base course module called {m_name} is the following:"
100                ) + &push_exercises_scores_progress(
101                    module_progress,
102                    progress_info.automatic_completion,
103                    progress_info.requires_exam,
104                    "module",
105                ) + "To pass the course, it's required to pass the base module. The following modules are additional to the course and to complete them, it's required to first complete the base module.\n"
106            } else {
107                // If the `progress` vec is empty, then:
108                "There is no progress information for this user on this course. ".to_string()
109            };
110            res += &s;
111
112            // skip first because we processed it earlier and add the progress for
113            // each module
114            for progress_info in progress.iter().skip(1) {
115                let module_progress = &progress_info.progress;
116                let m_name = &module_progress.course_module_name;
117                res.push_str(&format!(
118                    "The user's progress on the course module called {m_name} is the following:"
119                ));
120                res += &push_exercises_scores_progress(
121                    module_progress,
122                    progress_info.automatic_completion,
123                    progress_info.requires_exam,
124                    "module",
125                );
126            }
127        }
128        res
129    }
130
131    fn output_description_instructions(&self) -> Option<String> {
132        if self.state.progress.len() > 1 {
133            Some(
134            "Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. If the user asks something like 'how to pass the course', describe the passing criteria and requirements of the base module. Encourage the user to ask further questions about other modules if needed.".to_string(),
135        )
136        } else {
137            Some(
138            "Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.".to_string(),
139        )
140        }
141    }
142
143    fn get_arguments(&self) -> &Self::Arguments {
144        &self.arguments
145    }
146
147    fn get_tool_definition() -> AzureLLMFunctionToolDefinition {
148        AzureLLMFunctionToolDefinition {
149            tool_type: LLMToolType::Function,
150            name: "course_progress".to_string(),
151            description: "Get the user's progress on this course, including information about exercises attempted, points gained, the passing criteria for the course and if the user meets the criteria.".to_string(),
152            parameters: LLMToolParams {tool_type: LLMToolParamType::Object, properties: HashMap::new(), required: vec![], additional_properties: false},
153            strict: true
154        }
155    }
156}
157
158#[derive(serde::Serialize, serde::Deserialize)]
159pub struct CourseProgressArguments {}
160
161pub struct CourseProgressState {
162    course_name: String,
163    progress: Vec<CourseProgressInfo>,
164}
165
166/// Contains the info needed to create course progress outputs for a user
167#[derive(Debug, PartialEq, Clone)]
168pub struct CourseProgressInfo {
169    order_number: i32,
170    progress: UserCourseProgress,
171    automatic_completion: bool,
172    requires_exam: bool,
173}
174
175fn push_exercises_scores_progress(
176    module_progress: &UserCourseProgress,
177    automatic_completion: bool,
178    requires_exam: bool,
179    course_or_module: &str,
180) -> String {
181    let attempted_exercises = module_progress.attempted_exercises;
182    let total_exercises = module_progress.total_exercises;
183    let attempted_exercises_required = module_progress.attempted_exercises_required;
184    let score_given = module_progress.score_given;
185    let score_maximum = module_progress.score_maximum;
186    let score_required = module_progress.score_required;
187
188    let mut res = "".to_string();
189    if total_exercises.is_some() || score_maximum.is_some() {
190        if let (Some(a), Some(b)) = (total_exercises, score_maximum)
191            && a == 0
192            && b == 0
193        {
194            res += &format!(
195                " This {course_or_module} has no exercises and no points. It cannot be completed by doing exercises."
196            );
197            if requires_exam {
198                res += " Passing an exam is required for completion.";
199            } else {
200                res += &format!(
201                    " The user should look for information about completing the {course_or_module} in the course material or contact the teacher.\n"
202                );
203            }
204            return res;
205        }
206        res += &format!(" On this {course_or_module}, there are available a total of ");
207
208        if let Some(a) = total_exercises {
209            res += &format!("{a} exercises");
210        }
211        if let Some(b) = score_maximum {
212            if total_exercises.is_some() {
213                res += " and ";
214            }
215            res += &format!("{b} exercise points");
216        }
217        res += ".";
218    }
219    if automatic_completion && score_required.is_none() && attempted_exercises_required.is_none() {
220        res += &format!(
221            " It's not required to attempt exercises or gain points to pass this {course_or_module}."
222        );
223    }
224
225    if requires_exam {
226        res += &format!(" To pass this {course_or_module}, it's required to complete an exam.");
227    }
228    if !automatic_completion {
229        res += &format!(
230            " This {course_or_module} is graded by a teacher and can't be automatically passed by completing exercises. The user should look for information about completing the {course_or_module} in the course material or contact the teacher."
231        );
232    }
233
234    if attempted_exercises_required.is_some() || score_required.is_some() {
235        if requires_exam {
236            res += " To be qualified to take the exam, it's required to ";
237        } else {
238            res += &format!(" To pass this {course_or_module}, it's required to ");
239        }
240
241        if let Some(a) = attempted_exercises_required {
242            res += &format!("attempt {a} exercises");
243        }
244        if let Some(b) = score_required {
245            if attempted_exercises_required.is_some() {
246                res += " and ";
247            }
248            res += &format!("gain {b} exercise points");
249        }
250        res += ".";
251    } else if requires_exam {
252        res += " The user can attempt the exam regardless of their progress on the course."
253    }
254
255    if let Some(b) = attempted_exercises {
256        res += &format!(" The user has attempted {b} exercises.");
257    } else {
258        res += " The user has not attempted any exercises.";
259    }
260    let attempted_exercises_n = attempted_exercises.unwrap_or(0);
261
262    let pass = if requires_exam {
263        "be qualified to take the exam".to_string()
264    } else {
265        format!("pass this {course_or_module}")
266    };
267
268    if let Some(c) = attempted_exercises_required {
269        let ex_left = c - attempted_exercises_n;
270        if ex_left <= 0 {
271            res += &format!(
272                " They meet the criteria to {pass} if they have also received enough points."
273            );
274        } else {
275            res += &format!(" To {pass}, they need to attempt {ex_left} more exercises.");
276        }
277    }
278
279    // round down to one digit
280    let score = (score_given * 10.0).floor() / 10.0;
281    res += &format!(" The user has gained {:.1} points.", score);
282    if let Some(e) = score_required {
283        let pts_left = e as f32 - score;
284        if pts_left <= 0 as f32 {
285            res += &format!(" The user has gained enough points to {pass}.")
286        } else {
287            res += &format!(
288                " To {pass}, the user needs to gain {:.1} more points.",
289                pts_left
290            )
291        }
292    }
293    res + "\n"
294}
295
296/// Combine UserCourseProgress with the CompletionPolicy from an associated CourseModule.
297fn progress_info(
298    user_progress: Vec<UserCourseProgress>,
299    modules: Vec<CourseModule>,
300    course_name: &str,
301) -> ChatbotResult<Vec<CourseProgressInfo>> {
302    user_progress
303        .into_iter()
304        .map(|u| {
305            let module = modules
306                .iter()
307                .find(|x| x.order_number == u.course_module_order_number);
308            if let Some(m) = module {
309                let (automatic_completion, requires_exam) = match &m.completion_policy {
310                    CompletionPolicy::Automatic(policy) => (true, policy.requires_exam),
311                    CompletionPolicy::Manual => (false, false),
312                };
313                Ok(CourseProgressInfo {
314                    order_number: u.course_module_order_number,
315                    progress: u,
316                    automatic_completion,
317                    requires_exam,
318                })
319            } else {
320                Err(chatbot_err!(Other, format!("There was an error fetching the user's course progress information. Couldn't find course module {} of course {}.", u.course_module_name, course_name)))
321            }
322        })
323        .collect::<ChatbotResult<Vec<CourseProgressInfo>>>()
324}
325
326#[cfg(test)]
327mod tests {
328    use uuid::Uuid;
329
330    use super::*;
331
332    impl CourseProgressTool {
333        fn new_mock(course_name: String, progress: Vec<CourseProgressInfo>) -> Self {
334            CourseProgressTool {
335                state: CourseProgressState {
336                    course_name,
337                    progress,
338                },
339                arguments: CourseProgressArguments {},
340            }
341        }
342    }
343
344    #[test]
345    fn test_course_progress_output_only_base_module() {
346        let progress = vec![CourseProgressInfo {
347            order_number: 1,
348            progress: UserCourseProgress {
349                course_module_id: Uuid::nil(),
350                course_module_name: "Example base module".to_string(),
351                course_module_order_number: 1,
352                score_given: 3.3,
353                score_required: Some(4),
354                score_maximum: Some(5),
355                total_exercises: Some(11),
356                attempted_exercises: Some(4),
357                attempted_exercises_required: Some(10),
358            },
359            automatic_completion: true,
360            requires_exam: false,
361        }];
362        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
363        let output = tool.get_tool_output();
364
365        let expected_output =
366"Result: [output]The user is completing a course called Advanced Chatbot Course. Their progress on this course is the following: On this course, there are available a total of 11 exercises and 5 exercise points. To pass this course, it's required to attempt 10 exercises and gain 4 exercise points. The user has attempted 4 exercises. To pass this course, they need to attempt 6 more exercises. The user has gained 3.3 points. To pass this course, the user needs to gain 0.7 more points.\n[/output]
367
368Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.[/instructions]".to_string();
369
370        assert_eq!(output, expected_output);
371    }
372
373    #[test]
374    fn test_course_progress_output_many_modules() {
375        let progress = vec![
376            CourseProgressInfo {
377                order_number: 3,
378                progress: UserCourseProgress {
379                    course_module_id: Uuid::nil(),
380                    course_module_name: "Second extra module".to_string(),
381                    course_module_order_number: 3,
382                    score_given: 0.0,
383                    score_required: Some(4),
384                    score_maximum: Some(5),
385                    total_exercises: Some(6),
386                    attempted_exercises: None,
387                    attempted_exercises_required: Some(5),
388                },
389                automatic_completion: true,
390                requires_exam: false,
391            },
392            CourseProgressInfo {
393                order_number: 1,
394                progress: UserCourseProgress {
395                    course_module_id: Uuid::nil(),
396                    course_module_name: "Advanced Chatbot Course".to_string(),
397                    course_module_order_number: 1,
398                    score_given: 8.056,
399                    score_required: Some(8),
400                    score_maximum: Some(10),
401                    total_exercises: Some(5),
402                    attempted_exercises: Some(5),
403                    attempted_exercises_required: Some(5),
404                },
405                automatic_completion: true,
406                requires_exam: false,
407            },
408            CourseProgressInfo {
409                order_number: 2,
410                progress: UserCourseProgress {
411                    course_module_id: Uuid::nil(),
412                    course_module_name: "First extra module".to_string(),
413                    course_module_order_number: 2,
414                    score_given: 3.94,
415                    score_required: Some(5),
416                    score_maximum: Some(6),
417                    total_exercises: Some(6),
418                    attempted_exercises: Some(4),
419                    attempted_exercises_required: Some(5),
420                },
421                automatic_completion: true,
422                requires_exam: false,
423            },
424            CourseProgressInfo {
425                order_number: 4,
426                progress: UserCourseProgress {
427                    course_module_id: Uuid::nil(),
428                    course_module_name: "Chatbot advanced topics".to_string(),
429                    course_module_order_number: 4,
430                    score_given: 2.0,
431                    score_required: None,
432                    score_maximum: None,
433                    total_exercises: Some(2),
434                    attempted_exercises: Some(2),
435                    attempted_exercises_required: None,
436                },
437                automatic_completion: true,
438                requires_exam: false,
439            },
440        ];
441
442        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
443        let output = tool.get_tool_output();
444
445        let expected_output =
446"Result: [output]The user is completing a course called Advanced Chatbot Course. The course has one base module, and additional modules. The user's progress on the base course module called Advanced Chatbot Course is the following: On this module, there are available a total of 5 exercises and 10 exercise points. To pass this module, it's required to attempt 5 exercises and gain 8 exercise points. The user has attempted 5 exercises. They meet the criteria to pass this module if they have also received enough points. The user has gained 8.0 points. The user has gained enough points to pass this module.
447To pass the course, it's required to pass the base module. The following modules are additional to the course and to complete them, it's required to first complete the base module.
448The user's progress on the course module called First extra module is the following: On this module, there are available a total of 6 exercises and 6 exercise points. To pass this module, it's required to attempt 5 exercises and gain 5 exercise points. The user has attempted 4 exercises. To pass this module, they need to attempt 1 more exercises. The user has gained 3.9 points. To pass this module, the user needs to gain 1.1 more points.
449The user's progress on the course module called Second extra module is the following: On this module, there are available a total of 6 exercises and 5 exercise points. To pass this module, it's required to attempt 5 exercises and gain 4 exercise points. The user has not attempted any exercises. To pass this module, they need to attempt 5 more exercises. The user has gained 0.0 points. To pass this module, the user needs to gain 4.0 more points.
450The user's progress on the course module called Chatbot advanced topics is the following: On this module, there are available a total of 2 exercises. It's not required to attempt exercises or gain points to pass this module. The user has attempted 2 exercises. The user has gained 2.0 points.\n[/output]
451
452Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. If the user asks something like 'how to pass the course', describe the passing criteria and requirements of the base module. Encourage the user to ask further questions about other modules if needed.[/instructions]".to_string();
453
454        assert_eq!(output, expected_output);
455    }
456
457    #[test]
458    fn test_course_progress_output_no_progress() {
459        let progress: Vec<CourseProgressInfo> = vec![];
460
461        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
462        let output = tool.get_tool_output();
463
464        let expected_output =
465"Result: [output]The user is completing a course called Advanced Chatbot Course. There is no progress information for this user on this course. [/output]
466
467Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.[/instructions]".to_string();
468
469        assert_eq!(output, expected_output);
470    }
471
472    #[test]
473    fn test_course_progress_output_no_course_points_exercises() {
474        let progress = vec![CourseProgressInfo {
475            order_number: 1,
476            progress: UserCourseProgress {
477                course_module_id: Uuid::nil(),
478                course_module_name: "Example base module".to_string(),
479                course_module_order_number: 1,
480                score_given: 0.0,
481                score_required: None,
482                score_maximum: Some(0),
483                total_exercises: Some(0),
484                attempted_exercises: None,
485                attempted_exercises_required: None,
486            },
487            automatic_completion: true,
488            requires_exam: false,
489        }];
490        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
491        let output = tool.get_tool_output();
492
493        let expected_output =
494"Result: [output]The user is completing a course called Advanced Chatbot Course. Their progress on this course is the following: This course has no exercises and no points. It cannot be completed by doing exercises. The user should look for information about completing the course in the course material or contact the teacher.\n[/output]
495
496Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.[/instructions]".to_string();
497
498        assert_eq!(output, expected_output);
499    }
500
501    #[test]
502    fn test_course_progress_output_cant_be_completed() {
503        let progress = vec![CourseProgressInfo {
504            order_number: 1,
505            progress: UserCourseProgress {
506                course_module_id: Uuid::nil(),
507                course_module_name: "Example base module".to_string(),
508                course_module_order_number: 1,
509                score_given: 0.0,
510                score_required: Some(9),
511                score_maximum: Some(10),
512                total_exercises: Some(10),
513                attempted_exercises: None,
514                attempted_exercises_required: Some(10),
515            },
516            // cannot be completed automatically
517            automatic_completion: false,
518            requires_exam: false,
519        }];
520        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
521        let output = tool.get_tool_output();
522
523        let expected_output =
524"Result: [output]The user is completing a course called Advanced Chatbot Course. Their progress on this course is the following: On this course, there are available a total of 10 exercises and 10 exercise points. This course is graded by a teacher and can't be automatically passed by completing exercises. The user should look for information about completing the course in the course material or contact the teacher. To pass this course, it's required to attempt 10 exercises and gain 9 exercise points. The user has not attempted any exercises. To pass this course, they need to attempt 10 more exercises. The user has gained 0.0 points. To pass this course, the user needs to gain 9.0 more points.\n[/output]
525
526Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.[/instructions]".to_string();
527
528        assert_eq!(output, expected_output);
529    }
530
531    #[test]
532    fn test_course_progress_output_exercises_required_none() {
533        let progress = vec![CourseProgressInfo {
534            order_number: 1,
535            progress: UserCourseProgress {
536                course_module_id: Uuid::nil(),
537                course_module_name: "Example base module".to_string(),
538                course_module_order_number: 1,
539                score_given: 0.0,
540                score_required: Some(9),
541                score_maximum: Some(10),
542                total_exercises: Some(10),
543                attempted_exercises: None,
544                attempted_exercises_required: None,
545            },
546            automatic_completion: true,
547            requires_exam: false,
548        }];
549        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
550        let output = tool.get_tool_output();
551
552        let expected_output =
553"Result: [output]The user is completing a course called Advanced Chatbot Course. Their progress on this course is the following: On this course, there are available a total of 10 exercises and 10 exercise points. To pass this course, it's required to gain 9 exercise points. The user has not attempted any exercises. The user has gained 0.0 points. To pass this course, the user needs to gain 9.0 more points.\n[/output]
554
555Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.[/instructions]".to_string();
556
557        assert_eq!(output, expected_output);
558    }
559
560    #[test]
561    fn test_course_progress_output_pts_required_none() {
562        let progress = vec![CourseProgressInfo {
563            order_number: 1,
564            progress: UserCourseProgress {
565                course_module_id: Uuid::nil(),
566                course_module_name: "Example base module".to_string(),
567                course_module_order_number: 1,
568                score_given: 0.0,
569                score_required: None,
570                score_maximum: Some(10),
571                total_exercises: Some(10),
572                attempted_exercises: None,
573                attempted_exercises_required: Some(10),
574            },
575            automatic_completion: true,
576            requires_exam: false,
577        }];
578        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
579        let output = tool.get_tool_output();
580
581        let expected_output =
582"Result: [output]The user is completing a course called Advanced Chatbot Course. Their progress on this course is the following: On this course, there are available a total of 10 exercises and 10 exercise points. To pass this course, it's required to attempt 10 exercises. The user has not attempted any exercises. To pass this course, they need to attempt 10 more exercises. The user has gained 0.0 points.\n[/output]
583
584Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.[/instructions]".to_string();
585
586        assert_eq!(output, expected_output);
587    }
588
589    #[test]
590    fn test_course_progress_output_exam_required() {
591        let progress = vec![CourseProgressInfo {
592            order_number: 1,
593            progress: UserCourseProgress {
594                course_module_id: Uuid::nil(),
595                course_module_name: "Example base module".to_string(),
596                course_module_order_number: 1,
597                score_given: 0.0780006,
598                score_required: Some(9),
599                score_maximum: Some(10),
600                total_exercises: Some(10),
601                attempted_exercises: None,
602                attempted_exercises_required: Some(10),
603            },
604            automatic_completion: true,
605            requires_exam: true,
606        }];
607        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
608        let output = tool.get_tool_output();
609
610        let expected_output =
611"Result: [output]The user is completing a course called Advanced Chatbot Course. Their progress on this course is the following: On this course, there are available a total of 10 exercises and 10 exercise points. To pass this course, it's required to complete an exam. To be qualified to take the exam, it's required to attempt 10 exercises and gain 9 exercise points. The user has not attempted any exercises. To be qualified to take the exam, they need to attempt 10 more exercises. The user has gained 0.0 points. To be qualified to take the exam, the user needs to gain 9.0 more points.\n[/output]
612
613Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.[/instructions]".to_string();
614
615        assert_eq!(output, expected_output);
616    }
617
618    #[test]
619    fn test_course_progress_output_exam_required_can_do_exam() {
620        let progress = vec![CourseProgressInfo {
621            order_number: 1,
622            progress: UserCourseProgress {
623                course_module_id: Uuid::nil(),
624                course_module_name: "Example base module".to_string(),
625                course_module_order_number: 1,
626                score_given: 9.00006,
627                score_required: Some(9),
628                score_maximum: Some(10),
629                total_exercises: Some(10),
630                attempted_exercises: Some(10),
631                attempted_exercises_required: Some(10),
632            },
633            automatic_completion: true,
634            requires_exam: true,
635        }];
636        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
637        let output = tool.get_tool_output();
638
639        let expected_output =
640"Result: [output]The user is completing a course called Advanced Chatbot Course. Their progress on this course is the following: On this course, there are available a total of 10 exercises and 10 exercise points. To pass this course, it's required to complete an exam. To be qualified to take the exam, it's required to attempt 10 exercises and gain 9 exercise points. The user has attempted 10 exercises. They meet the criteria to be qualified to take the exam if they have also received enough points. The user has gained 9.0 points. The user has gained enough points to be qualified to take the exam.\n[/output]
641
642Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.[/instructions]".to_string();
643
644        assert_eq!(output, expected_output);
645    }
646
647    #[test]
648    fn test_course_progress_output_exam_only_required() {
649        let progress = vec![CourseProgressInfo {
650            order_number: 1,
651            progress: UserCourseProgress {
652                course_module_id: Uuid::nil(),
653                course_module_name: "Example base module".to_string(),
654                course_module_order_number: 1,
655                score_given: 9.0,
656                score_required: None,
657                score_maximum: Some(10),
658                total_exercises: Some(10),
659                attempted_exercises: Some(10),
660                attempted_exercises_required: None,
661            },
662            automatic_completion: true,
663            requires_exam: true,
664        }];
665        let tool = CourseProgressTool::new_mock("Advanced Chatbot Course".to_string(), progress);
666        let output = tool.get_tool_output();
667
668        let expected_output =
669"Result: [output]The user is completing a course called Advanced Chatbot Course. Their progress on this course is the following: On this course, there are available a total of 10 exercises and 10 exercise points. It's not required to attempt exercises or gain points to pass this course. To pass this course, it's required to complete an exam. The user can attempt the exam regardless of their progress on the course. The user has attempted 10 exercises. The user has gained 9.0 points.\n[/output]
670
671Instructions for describing the output: [instructions]Describe this information in a short, clear way with no or minimal bullet points. Only give information that is relevant to the user's question. Encourage the user to ask further questions if needed.[/instructions]".to_string();
672
673        assert_eq!(output, expected_output);
674    }
675}