Skip to main content

headless_lms_chatbot/chatbot_tools/custom_tools/
course_progress.rs

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