headless_lms_server/controllers/course_material/
chatbot.rs

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