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::chatbot_conversation_messages::MessageRole;
7use headless_lms_models::chatbot_conversations::{
8    self, ChatbotConversation, ChatbotConversationInfo,
9};
10use headless_lms_models::{chatbot_configurations, courses};
11
12use crate::prelude::*;
13
14/**
15GET `/api/v0/course-material/course-modules/chatbot/default-for-course/:course-id`
16
17Returns the default chatbot configuration id for a course if the default chatbot is enabled to students.
18*/
19#[instrument(skip(pool))]
20async fn get_default_chatbot_configuration_for_course(
21    pool: web::Data<PgPool>,
22    course_id: web::Path<Uuid>,
23) -> ControllerResult<web::Json<Option<Uuid>>> {
24    let token = skip_authorize();
25
26    let mut conn = pool.acquire().await?;
27    let chatbot_configurations =
28        models::chatbot_configurations::get_for_course(&mut conn, *course_id).await?;
29
30    let res = chatbot_configurations
31        .into_iter()
32        .filter(|c| c.enabled_to_students)
33        .find(|c| c.default_chatbot)
34        .map(|c| c.id);
35
36    token.authorized_ok(web::Json(res))
37}
38
39/**
40POST `/api/v0/course-material/chatbot/:chatbot_configuration_id/conversations/:conversation_id/send-message`
41
42Sends a new chat message to the chatbot.
43*/
44#[instrument(skip(pool, app_conf))]
45async fn send_message(
46    pool: web::Data<PgPool>,
47    params: web::Path<(Uuid, Uuid)>,
48    user: AuthUser,
49    app_conf: web::Data<ApplicationConfiguration>,
50    payload: web::Json<String>,
51) -> ControllerResult<HttpResponse> {
52    let message = payload.into_inner();
53    let chatbot_configuration_id = params.0;
54    let conversation_id = params.1;
55    let mut conn = pool.acquire().await?;
56    let mut tx: sqlx::Transaction<Postgres> = conn.begin().await?;
57    let course_id = chatbot_configurations::get_by_id(&mut tx, chatbot_configuration_id)
58        .await?
59        .course_id;
60    let course_name = courses::get_course(&mut tx, course_id).await?.name;
61    let chatbot_user = ChatbotUserContext {
62        user_id: user.id.to_owned(),
63        course_id,
64        course_name,
65    };
66    let token = skip_authorize();
67
68    let response_stream = send_chat_request_and_parse_stream(
69        &mut tx,
70        // An Arc, cheap to clone.
71        pool.get_ref().clone(),
72        &app_conf,
73        chatbot_configuration_id,
74        conversation_id,
75        &message,
76        chatbot_user,
77    )
78    .await?;
79
80    tx.commit().await?;
81
82    token.authorized_ok(
83        HttpResponse::Ok()
84            .content_type(ContentType::json())
85            .streaming(response_stream),
86    )
87}
88
89/**
90POST `/api/v0/course-material/course-modules/chatbot/:chatbot_configuration_id/conversations/new`
91
92Sends a new chat message to the chatbot.
93*/
94#[instrument(skip(pool))]
95async fn new_conversation(
96    pool: web::Data<PgPool>,
97    user: AuthUser,
98    params: web::Path<Uuid>,
99) -> ControllerResult<web::Json<ChatbotConversation>> {
100    let token = skip_authorize();
101
102    let mut conn = pool.acquire().await?;
103    let mut tx = conn.begin().await?;
104
105    let configuration = models::chatbot_configurations::get_by_id(&mut tx, *params).await?;
106
107    let conversation = models::chatbot_conversations::insert(
108        &mut tx,
109        ChatbotConversation {
110            id: Uuid::new_v4(),
111            created_at: Utc::now(),
112            updated_at: Utc::now(),
113            deleted_at: None,
114            course_id: configuration.course_id,
115            user_id: user.id,
116            chatbot_configuration_id: configuration.id,
117        },
118    )
119    .await?;
120
121    let _first_message = models::chatbot_conversation_messages::insert(
122        &mut tx,
123        models::chatbot_conversation_messages::ChatbotConversationMessage {
124            id: Uuid::new_v4(),
125            created_at: Utc::now(),
126            updated_at: Utc::now(),
127            deleted_at: None,
128            conversation_id: conversation.id,
129            message: Some(configuration.initial_message.clone()),
130            message_role: MessageRole::Assistant,
131            message_is_complete: true,
132            used_tokens: estimate_tokens(&configuration.initial_message),
133            order_number: 0,
134            tool_output: None,
135            tool_call_fields: vec![],
136        },
137    )
138    .await?;
139
140    tx.commit().await?;
141
142    token.authorized_ok(web::Json(conversation))
143}
144
145/**
146POST `/api/v0/course-material/course-modules/chatbot/:chatbot_configuration_id/conversations/current`
147
148Returns the current conversation for the user.
149*/
150#[instrument(skip(pool))]
151async fn current_conversation_info(
152    pool: web::Data<PgPool>,
153    user: AuthUser,
154    params: web::Path<Uuid>,
155) -> ControllerResult<web::Json<ChatbotConversationInfo>> {
156    let token = skip_authorize();
157
158    let mut conn = pool.acquire().await?;
159    let chatbot_configuration =
160        models::chatbot_configurations::get_by_id(&mut conn, *params).await?;
161    let res = chatbot_conversations::get_current_conversation_info(
162        &mut conn,
163        user.id,
164        chatbot_configuration.id,
165    )
166    .await?;
167
168    token.authorized_ok(web::Json(res))
169}
170
171/**
172Add a route for each controller in this module.
173
174The name starts with an underline in order to appear before other functions in the module documentation.
175
176We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
177*/
178pub fn _add_routes(cfg: &mut ServiceConfig) {
179    cfg.route(
180        "/{chatbot_configuration_id}/conversations/{conversation_id}/send-message",
181        web::post().to(send_message),
182    )
183    .route(
184        "/{chatbot_configuration_id}/conversations/current",
185        web::get().to(current_conversation_info),
186    )
187    .route(
188        "/{chatbot_configuration_id}/conversations/new",
189        web::post().to(new_conversation),
190    )
191    .route(
192        "/default-for-course/{course_id}",
193        web::get().to(get_default_chatbot_configuration_for_course),
194    );
195}