headless_lms_chatbot/chatbot_tools/custom_tools/
course_structure.rs1use 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 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 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 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}