Skip to main content

headless_lms_chatbot/chatbot_tools/custom_tools/
course_structure.rs

1use std::collections::HashMap;
2
3use headless_lms_models::pages;
4use headless_lms_utils::document_schema_processor::get_learning_objectives;
5use sqlx::PgConnection;
6
7use crate::{
8    azure_chatbot::ChatbotUserContext,
9    chatbot_error::chatbot_err,
10    chatbot_tools::{
11        AzureLLMFunctionToolDefinition, ChatbotTool, LLMToolParamType, LLMToolParams, LLMToolType,
12        ToolProperties,
13    },
14    prelude::{BackendError, ChatbotError, ChatbotErrorType, ChatbotResult},
15};
16
17pub type CourseStructureTool = ToolProperties<CourseStructureState, CourseStructureArguments>;
18
19pub struct CourseStructureState {
20    course_pages_info: Vec<PageDocumentInfo>,
21}
22
23#[derive(serde::Serialize, serde::Deserialize, Debug)]
24#[serde(rename_all = "snake_case")]
25pub enum PageType {
26    CourseFrontPage,
27    TopLevelPage,
28    ChapterFrontPage,
29    GenericPage,
30}
31
32impl PageType {
33    /// Determine page type based on page's position in course structure
34    fn determine(
35        order_number: i32,
36        chapter_number: Option<i32>,
37        module_number: Option<i32>,
38    ) -> Self {
39        if chapter_number.is_none() && module_number.is_none() && order_number == 0 {
40            PageType::CourseFrontPage
41        } else if chapter_number.is_none() && module_number.is_none() && order_number != 0 {
42            PageType::TopLevelPage
43        } else if chapter_number.is_some() && order_number == 0 {
44            PageType::ChapterFrontPage
45        } else {
46            PageType::GenericPage
47        }
48    }
49}
50
51#[derive(serde::Serialize, serde::Deserialize, Debug)]
52pub struct PageDocumentInfo {
53    pub page_title: String,
54    pub page_type: PageType,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub chapter_title: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub chapter_number: Option<i32>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub module_name: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub learning_objectives: Option<String>,
63}
64
65#[derive(serde::Serialize, serde::Deserialize)]
66pub struct CourseStructureArguments {}
67
68impl ChatbotTool for CourseStructureTool {
69    type State = CourseStructureState;
70    type Arguments = CourseStructureArguments;
71
72    fn parse_arguments(_args_string: String) -> ChatbotResult<Self::Arguments> {
73        Ok(CourseStructureArguments {})
74    }
75
76    async fn from_db_and_arguments(
77        conn: &mut PgConnection,
78        arguments: Self::Arguments,
79        user_context: &ChatbotUserContext,
80    ) -> ChatbotResult<Self>
81    where
82        Self: Sized,
83    {
84        let Some(course_id) = user_context.course_id else {
85            return Err(chatbot_err!(
86                ToolUseError,
87                "Course id is missing.".to_string()
88            ));
89        };
90
91        let mut pages_info = pages::get_page_info_special_for_course(conn, course_id)
92            .await
93            .map_err(ChatbotError::from)?;
94        pages_info.sort_by_key(|x| {
95            // map module number 0 to 1 so that pages without a module
96            // are ordered first. same for chapters.
97            // order by module first, then chapter, then page number.
98            x.module_number.map(|x| x + 1).unwrap_or(0) * 100
99                + x.chapter_number.map(|x| x + 1).unwrap_or(0) * 10
100                + x.order_number
101        });
102
103        let info: Vec<PageDocumentInfo> = pages_info
104            .into_iter()
105            .map(|p| {
106                let blocks = p.blocks_cloned();
107                let Ok(b) = blocks else {
108                    // no block content
109                    return PageDocumentInfo {
110                        page_title: p.page_title,
111                        page_type: PageType::determine(
112                            p.order_number,
113                            p.chapter_number,
114                            p.module_number,
115                        ),
116                        chapter_title: p.chapter_title,
117                        learning_objectives: None,
118                        chapter_number: p.chapter_number,
119                        module_name: p.module_name,
120                    };
121                };
122                let learning_objectives = get_learning_objectives(b).ok();
123                PageDocumentInfo {
124                    page_title: p.page_title,
125                    page_type: PageType::determine(
126                        p.order_number,
127                        p.chapter_number,
128                        p.module_number,
129                    ),
130                    chapter_title: p.chapter_title,
131                    learning_objectives,
132                    chapter_number: p.chapter_number,
133                    module_name: p.module_name,
134                }
135            })
136            .collect();
137
138        Ok(CourseStructureTool {
139            state: CourseStructureState {
140                course_pages_info: info,
141            },
142            arguments,
143        })
144    }
145
146    fn output(&self) -> String {
147        serde_json::to_string(&self.state.course_pages_info).unwrap_or("Not found.".to_string())
148    }
149
150    fn output_description_instructions(&self) -> Option<String> {
151        Some("The user has access to the course structure, so you shouldn't give it to them: they know it already. You can give an overview if asked. Use the course structure to find out more about the course and answer the user's questions. You can look up the content of the listed course pages with the document_lookup tool. The learning objectives listed on the course front page or top level pages are objectives for the whole course. Learning objectives listed on a chapter front page encompass the whole chapter, and objectives listed on a generic page are for the page only.".to_string())
152    }
153
154    fn get_arguments(&self) -> &Self::Arguments {
155        &self.arguments
156    }
157
158    fn get_tool_definition() -> AzureLLMFunctionToolDefinition {
159        AzureLLMFunctionToolDefinition {
160            tool_type: LLMToolType::Function,
161            name: "course_structure".to_string(),
162            description: "Get the course structure as an ordered list of all course pages. The structure lists all pages, chapters and modules that are part of the course. Each page is listed with its title, its place in the course structure (which chapter it is inside of, if any), and its learning objectives, if any. Information about the course pages' content can be found with the document_lookup tool.".to_string(),
163            parameters: LLMToolParams {
164                tool_type: LLMToolParamType::Object,
165                properties: HashMap::new(),
166                required: vec![],
167                additional_properties: false,
168            },
169            strict: true,
170        }
171    }
172}