Skip to main content

headless_lms_chatbot/
course_description_summary.rs

1use headless_lms_utils::services::sisu::SisuDescriptions;
2use std::collections::HashMap;
3
4use crate::{
5    azure_chatbot::{
6        ArrayItem, ArrayProperty, InputItem, JSONType, JsonItem, LLMRequest, LLMRequestParams,
7        LLMRequestResponseFormatParam, NonThinkingParams, RequestTextOptions, Schema,
8        SchemaPropertyType, ThinkingParams,
9    },
10    chatbot_error::chatbot_err,
11    llm_utils::{
12        APIInputMessage, MessageContent, make_blocking_llm_request, model_is_thinking,
13        parse_text_completion,
14    },
15    prelude::{ChatbotError, ChatbotErrorType, ChatbotResult},
16};
17use headless_lms_base::config::ApplicationConfiguration;
18use headless_lms_base::error::backend_error::BackendError;
19use headless_lms_models::{
20    application_task_default_language_models::TaskLMSpec,
21    chatbot_conversation_message_messages::MessageRole,
22};
23use utoipa::ToSchema;
24
25#[derive(serde::Serialize, serde::Deserialize, ToSchema, Debug)]
26pub struct SisuDescriptionResponse {
27    pub course_description: String,
28    pub audience: Vec<String>,
29    pub modules: Vec<Module>,
30}
31
32#[derive(serde::Serialize, serde::Deserialize, ToSchema, Debug)]
33pub struct Module {
34    pub course_code: String,
35    pub description: String,
36    pub prerequisites: Vec<String>,
37}
38
39// You are given different type of information for an university course. There can exist multiple modules for the course which are differentiated by the module code as the key. Your task is to generate a single description combining information from all different modules but also generate module specific descriptions and prerequisites for each module. The prerequisites should be given in a list of individual requisites.
40
41const SYSTEM_PROMPT: &str = r#"
42Your task is to
431. Generate a single general description combining the information from all the different modules behind the key "course_description".
442. Behind the "audience" key you should create an array with suitable audience types as items on the list. By default this should always be just "everyone", unless the course material information truly specifies suitable audience types. Audience types should be general, for example "students" or "veterans". If you output more specific audience types also include "everyone" in the list unless it can be understood that the course is not for everyone. It is fine to mention some groups in addition to "everyone" that would just mean that the course works particularly well for those groups but everyone can take it. Please note that remarks about which study programme can choose this course are bureocratic university boilerplate and does not necessarily indicate the audience the course is meant for. Audience types like "Bachelor's degree students" are too specific.
453. Behind the "modules" key, you will generate an array of items, where each item represents one module, and thus the array has as many items as there are module codes. Each module item inside the array will have three fields, "course_code", "description" and "prerequisites".
46  3.1 The "course_code" field will have the corresponing module code.
47  3.2 The "description" field will be a description summarized from all the information you are given on the specific module.
48  3.3 The "prerequisites" field will be an array, with each prerequisite differentiated as an item in the list.
49
50
51When generating the description:
52- Use the same language in the description that is used in the given information.
53- Use same style of writing as in the given information.
54- Ignore all the information that is not relevant for the course description.
55- Ignore all the html tags inside the given information.
56- When generating module descriptions don't use filler words such as 'this course', give only relevant information.
57
58Constraints:
59- Base the summarization only on the information given to you.
60- Only output the summarized description, nothing else.
61- The maximum length for the description is 100 words.
62- If there is only one module in the course, use exactly the same description for both course description and module description.
63
64Your output must follow the JSON schema exactly:
65{
66    "course_description": "...",
67    "audience": ["..."],
68    "modules": [
69        {
70            "course_code": "...",
71            "description": "...",
72            "prerequisites": ["...", "...", "..."]
73        }
74    ]
75}"#;
76
77pub const USER_PROMPT: &str = r#"Give description based on the given information."#;
78
79pub async fn generate_description(
80    app_config: &ApplicationConfiguration,
81    task_lm: TaskLMSpec,
82    sisu_course_info: HashMap<String, SisuDescriptions>,
83) -> ChatbotResult<SisuDescriptionResponse> {
84    let serialized_sisu_course_info = serde_json::to_string(&sisu_course_info)?;
85    let prompt: String = format!("{USER_PROMPT} Course information: {serialized_sisu_course_info}");
86
87    let system_prompt = APIInputMessage {
88        message_type: InputItem::Message {
89            role: MessageRole::System,
90            content: MessageContent::Text(SYSTEM_PROMPT.to_string()),
91        },
92    };
93
94    let user_prompt = APIInputMessage {
95        message_type: InputItem::Message {
96            role: MessageRole::User,
97            content: MessageContent::Text(prompt),
98        },
99    };
100
101    let (params, max_output_tokens) = if model_is_thinking(task_lm.model_type) {
102        (
103            LLMRequestParams::GPTThinking(ThinkingParams { reasoning: None }),
104            Some(7000),
105        )
106    } else {
107        (
108            LLMRequestParams::GPTNonThinking(NonThinkingParams {
109                temperature: None,
110                top_p: None,
111                frequency_penalty: None,
112                presence_penalty: None,
113            }),
114            Some(4000),
115        )
116    };
117
118    let chat_request = LLMRequest {
119        input: vec![system_prompt, user_prompt],
120        model: task_lm.model.to_owned(),
121        max_output_tokens,
122        tools: vec![],
123        tool_choice: None,
124        parallel_tool_calls: None,
125        params,
126        text: Some(RequestTextOptions {
127            verbosity: None,
128            format: Some(LLMRequestResponseFormatParam {
129                format_type: JSONType::JsonSchema,
130                name: "LLMDescriptionResponse".to_string(),
131                schema: Schema {
132                    type_field: JSONType::Object,
133                    properties: HashMap::from([
134                        (
135                            "course_description".to_string(),
136                            SchemaPropertyType::Item(JsonItem {
137                                type_field: JSONType::String,
138                            }),
139                        ),
140                        (
141                            "audience".to_string(),
142                            SchemaPropertyType::ArrayProperty(ArrayProperty {
143                                type_field: JSONType::Array,
144                                items: ArrayItem::JsonItem(JsonItem {
145                                    type_field: JSONType::String,
146                                }),
147                            }),
148                        ),
149                        (
150                            "modules".to_string(),
151                            SchemaPropertyType::ArrayProperty(ArrayProperty {
152                                type_field: JSONType::Array,
153                                items: ArrayItem::Schema(Schema {
154                                    type_field: JSONType::Object,
155                                    properties: HashMap::from([
156                                        (
157                                            "course_code".to_string(),
158                                            SchemaPropertyType::Item(JsonItem {
159                                                type_field: JSONType::String,
160                                            }),
161                                        ),
162                                        (
163                                            "description".to_string(),
164                                            SchemaPropertyType::Item(JsonItem {
165                                                type_field: JSONType::String,
166                                            }),
167                                        ),
168                                        (
169                                            "prerequisites".to_string(),
170                                            SchemaPropertyType::ArrayProperty(ArrayProperty {
171                                                type_field: JSONType::Array,
172                                                items: ArrayItem::JsonItem(JsonItem {
173                                                    type_field: JSONType::String,
174                                                }),
175                                            }),
176                                        ),
177                                    ]),
178                                    required: Vec::from([
179                                        "course_code".to_string(),
180                                        "description".to_string(),
181                                        "prerequisites".to_string(),
182                                    ]),
183                                    additional_properties: false,
184                                }),
185                            }),
186                        ),
187                    ]),
188                    required: Vec::from([
189                        "course_description".to_string(),
190                        "audience".to_string(),
191                        "modules".to_string(),
192                    ]),
193                    additional_properties: false,
194                },
195                strict: true,
196            }),
197        }),
198    };
199
200    let completion = make_blocking_llm_request(chat_request, app_config).await?;
201
202    let completion_content: &String = &parse_text_completion(completion)?;
203
204    let descriptions: SisuDescriptionResponse =
205        serde_json::from_str(completion_content).map_err(|_| {
206            chatbot_err!(
207                SisuDescriptionError,
208                "Sisu description LLM returned an incorrectly formatted response.".to_string()
209            )
210        })?;
211    Ok(descriptions)
212}