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