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