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