headless_lms_chatbot/chatbot_tools/
course_progress.rs

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