headless_lms_server/controllers/course_material/
chatbot.rs

1use actix_web::http::header::ContentType;
2use chrono::Utc;
3
4use headless_lms_chatbot::azure_chatbot::{ChatbotUserContext, send_chat_request_and_parse_stream};
5use headless_lms_chatbot::llm_utils::estimate_tokens;
6use headless_lms_models::application_task_default_language_models::ApplicationTask;
7use headless_lms_models::chatbot_conversation_messages::MessageRole;
8use headless_lms_models::chatbot_conversations::{
9    self, ChatbotConversation, ChatbotConversationInfo,
10};
11use headless_lms_models::{chatbot_configurations, courses};
12use rand::seq::IndexedRandom;
13
14use crate::prelude::*;
15
16/**
17GET `/api/v0/course-material/course-modules/chatbot/default-for-course/:course-id`
18
19Returns the default chatbot configuration id for a course if the default chatbot is enabled to students.
20*/
21#[instrument(skip(pool))]
22async fn get_default_chatbot_configuration_for_course(
23    pool: web::Data<PgPool>,
24    course_id: web::Path<Uuid>,
25) -> ControllerResult<web::Json<Option<Uuid>>> {
26    let token = skip_authorize();
27
28    let mut conn = pool.acquire().await?;
29    let chatbot_configurations =
30        models::chatbot_configurations::get_for_course(&mut conn, *course_id).await?;
31
32    let res = chatbot_configurations
33        .into_iter()
34        .filter(|c| c.enabled_to_students)
35        .find(|c| c.default_chatbot)
36        .map(|c| c.id);
37
38    token.authorized_ok(web::Json(res))
39}
40
41/**
42POST `/api/v0/course-material/chatbot/:chatbot_configuration_id/conversations/:conversation_id/send-message`
43
44Sends a new chat message to the chatbot.
45*/
46#[instrument(skip(pool, app_conf))]
47async fn send_message(
48    pool: web::Data<PgPool>,
49    params: web::Path<(Uuid, Uuid)>,
50    user: AuthUser,
51    app_conf: web::Data<ApplicationConfiguration>,
52    payload: web::Json<String>,
53) -> ControllerResult<HttpResponse> {
54    let message = payload.into_inner();
55    let chatbot_configuration_id = params.0;
56    let conversation_id = params.1;
57    let mut conn = pool.acquire().await?;
58    let mut tx: sqlx::Transaction<Postgres> = conn.begin().await?;
59    let course_id = chatbot_configurations::get_by_id(&mut tx, chatbot_configuration_id)
60        .await?
61        .course_id;
62    let course_name = courses::get_course(&mut tx, course_id).await?.name;
63    let chatbot_user = ChatbotUserContext {
64        user_id: user.id.to_owned(),
65        course_id,
66        course_name,
67    };
68    let token = skip_authorize();
69
70    let response_stream = send_chat_request_and_parse_stream(
71        &mut tx,
72        // An Arc, cheap to clone.
73        pool.get_ref().clone(),
74        &app_conf,
75        chatbot_configuration_id,
76        conversation_id,
77        &message,
78        chatbot_user,
79    )
80    .await?;
81
82    tx.commit().await?;
83
84    token.authorized_ok(
85        HttpResponse::Ok()
86            .content_type(ContentType::json())
87            .streaming(response_stream),
88    )
89}
90
91/**
92POST `/api/v0/course-material/course-modules/chatbot/:chatbot_configuration_id/conversations/new`
93
94Sends a new chat message to the chatbot.
95*/
96#[instrument(skip(pool))]
97async fn new_conversation(
98    pool: web::Data<PgPool>,
99    user: AuthUser,
100    params: web::Path<Uuid>,
101) -> ControllerResult<web::Json<ChatbotConversation>> {
102    let token = skip_authorize();
103
104    let mut conn = pool.acquire().await?;
105    let mut tx = conn.begin().await?;
106
107    let configuration = models::chatbot_configurations::get_by_id(&mut tx, *params).await?;
108
109    let conversation = models::chatbot_conversations::insert(
110        &mut tx,
111        ChatbotConversation {
112            id: Uuid::new_v4(),
113            created_at: Utc::now(),
114            updated_at: Utc::now(),
115            deleted_at: None,
116            course_id: configuration.course_id,
117            user_id: user.id,
118            chatbot_configuration_id: configuration.id,
119        },
120    )
121    .await?;
122
123    let _first_message = models::chatbot_conversation_messages::insert(
124        &mut tx,
125        models::chatbot_conversation_messages::ChatbotConversationMessage {
126            id: Uuid::new_v4(),
127            created_at: Utc::now(),
128            updated_at: Utc::now(),
129            deleted_at: None,
130            conversation_id: conversation.id,
131            message: Some(configuration.initial_message.clone()),
132            message_role: MessageRole::Assistant,
133            message_is_complete: true,
134            used_tokens: estimate_tokens(&configuration.initial_message),
135            order_number: 0,
136            tool_output: None,
137            tool_call_fields: vec![],
138        },
139    )
140    .await?;
141
142    tx.commit().await?;
143
144    token.authorized_ok(web::Json(conversation))
145}
146
147/**
148POST `/api/v0/course-material/course-modules/chatbot/:chatbot_configuration_id/conversations/current`
149
150Returns the current conversation for the user.
151*/
152#[instrument(skip(pool, app_conf))]
153async fn current_conversation_info(
154    pool: web::Data<PgPool>,
155    user: AuthUser,
156    app_conf: web::Data<ApplicationConfiguration>,
157    params: web::Path<Uuid>,
158) -> ControllerResult<web::Json<ChatbotConversationInfo>> {
159    let token = skip_authorize();
160
161    let mut conn = pool.acquire().await?;
162    let chatbot_configuration =
163        models::chatbot_configurations::get_by_id(&mut conn, *params).await?;
164    let res = chatbot_conversations::get_current_conversation_info(
165        &mut conn,
166        user.id,
167        chatbot_configuration.id,
168    )
169    .await?;
170
171    if chatbot_configuration.suggest_next_messages
172        // suggested_messages is None if suggest_next_messages=false
173        && let Some(suggested_messages) = &res.suggested_messages
174        && suggested_messages.is_empty()
175        && let Some(current_conversation_messages) = &res.current_conversation_messages
176        && let Some(last_message) = current_conversation_messages.last()
177    {
178        let initial_suggested_messages = if last_message.order_number == 0 {
179            // for the first message, get initial_suggested_messages
180            let initial_suggested_messages = chatbot_configuration
181                .initial_suggested_messages
182                .unwrap_or(vec![]);
183            // take 3 random elements
184            if initial_suggested_messages.len() > 3 {
185                let mut rng = rand::rng();
186                initial_suggested_messages
187                    .choose_multiple(&mut rng, 3)
188                    .cloned()
189                    .collect()
190            } else {
191                initial_suggested_messages
192            }
193        } else {
194            // for other messages, generate suggested messages
195            let course_description =
196                models::courses::get_course(&mut conn, chatbot_configuration.course_id)
197                    .await?
198                    .description;
199            let message_suggest_llm =
200                models::application_task_default_language_models::get_for_task(
201                    &mut conn,
202                    ApplicationTask::MessageSuggestion,
203                )
204                .await?;
205            headless_lms_chatbot::message_suggestion::generate_suggested_messages(
206                &app_conf,
207                message_suggest_llm,
208                current_conversation_messages,
209                chatbot_configuration.initial_suggested_messages,
210                &res.course_name,
211                course_description,
212            )
213            .await?
214        };
215
216        if !initial_suggested_messages.is_empty() {
217            headless_lms_models::chatbot_conversation_suggested_messages::insert_batch(
218                &mut conn,
219                &last_message.id,
220                initial_suggested_messages,
221            )
222            .await?;
223        }
224
225        let res = chatbot_conversations::get_current_conversation_info(
226            &mut conn,
227            user.id,
228            chatbot_configuration.id,
229        )
230        .await?;
231        return token.authorized_ok(web::Json(res));
232    }
233
234    token.authorized_ok(web::Json(res))
235}
236
237/**
238Add a route for each controller in this module.
239
240The name starts with an underline in order to appear before other functions in the module documentation.
241
242We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
243*/
244pub fn _add_routes(cfg: &mut ServiceConfig) {
245    cfg.route(
246        "/{chatbot_configuration_id}/conversations/{conversation_id}/send-message",
247        web::post().to(send_message),
248    )
249    .route(
250        "/{chatbot_configuration_id}/conversations/current",
251        web::get().to(current_conversation_info),
252    )
253    .route(
254        "/{chatbot_configuration_id}/conversations/new",
255        web::post().to(new_conversation),
256    )
257    .route(
258        "/default-for-course/{course_id}",
259        web::get().to(get_default_chatbot_configuration_for_course),
260    );
261}