headless_lms_server/controllers/course_material/
chatbot.rs1use 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;
13use utoipa::OpenApi;
14
15use crate::prelude::*;
16
17#[derive(OpenApi)]
18#[openapi(paths(
19 get_default_chatbot_configuration_for_course,
20 send_message,
21 new_conversation,
22 current_conversation_info
23))]
24pub(crate) struct CourseMaterialChatbotApiDoc;
25
26#[utoipa::path(
32 get,
33 path = "/default-for-course/{course_id}",
34 operation_id = "getDefaultChatbotConfigurationForCourse",
35 tag = "course-material-chatbot",
36 params(
37 ("course_id" = Uuid, Path, description = "Course id")
38 ),
39 responses(
40 (status = 200, description = "Default chatbot configuration id", body = Option<Uuid>)
41 )
42)]
43#[instrument(skip(pool))]
44async fn get_default_chatbot_configuration_for_course(
45 pool: web::Data<PgPool>,
46 course_id: web::Path<Uuid>,
47) -> ControllerResult<web::Json<Option<Uuid>>> {
48 let token = skip_authorize();
49
50 let mut conn = pool.acquire().await?;
51 let chatbot_configurations =
52 models::chatbot_configurations::get_for_course(&mut conn, *course_id).await?;
53
54 let res = chatbot_configurations
55 .into_iter()
56 .filter(|c| c.enabled_to_students)
57 .find(|c| c.default_chatbot)
58 .map(|c| c.id);
59
60 token.authorized_ok(web::Json(res))
61}
62
63#[utoipa::path(
69 post,
70 path = "/{chatbot_configuration_id}/conversations/{conversation_id}/send-message",
71 operation_id = "sendChatbotMessage",
72 tag = "course-material-chatbot",
73 params(
74 ("chatbot_configuration_id" = Uuid, Path, description = "Chatbot configuration id"),
75 ("conversation_id" = Uuid, Path, description = "Conversation id")
76 ),
77 request_body(
78 content = String,
79 content_type = "application/json"
80 ),
81 responses(
82 (status = 200, description = "Chatbot response stream", body = String)
83 )
84)]
85#[instrument(skip(pool, app_conf))]
86async fn send_message(
87 pool: web::Data<PgPool>,
88 params: web::Path<(Uuid, Uuid)>,
89 user: AuthUser,
90 app_conf: web::Data<ApplicationConfiguration>,
91 payload: web::Json<String>,
92) -> ControllerResult<HttpResponse> {
93 let message = payload.into_inner();
94 let chatbot_configuration_id = params.0;
95 let conversation_id = params.1;
96 let mut conn = pool.acquire().await?;
97 let mut tx: sqlx::Transaction<Postgres> = conn.begin().await?;
98 let course_id = chatbot_configurations::get_by_id(&mut tx, chatbot_configuration_id)
99 .await?
100 .course_id;
101 let course_name = courses::get_course(&mut tx, course_id).await?.name;
102 let chatbot_user = ChatbotUserContext {
103 user_id: user.id.to_owned(),
104 course_id,
105 course_name,
106 };
107 let token = skip_authorize();
108
109 let response_stream = send_chat_request_and_parse_stream(
110 &mut tx,
111 pool.get_ref().clone(),
113 &app_conf,
114 chatbot_configuration_id,
115 conversation_id,
116 &message,
117 chatbot_user,
118 )
119 .await?;
120
121 tx.commit().await?;
122
123 token.authorized_ok(
124 HttpResponse::Ok()
125 .content_type(ContentType::json())
126 .streaming(response_stream),
127 )
128}
129
130#[utoipa::path(
136 post,
137 path = "/{chatbot_configuration_id}/conversations/new",
138 operation_id = "newChatbotConversation",
139 tag = "course-material-chatbot",
140 params(
141 ("chatbot_configuration_id" = Uuid, Path, description = "Chatbot configuration id")
142 ),
143 responses(
144 (status = 200, description = "Created chatbot conversation", body = ChatbotConversation)
145 )
146)]
147#[instrument(skip(pool))]
148async fn new_conversation(
149 pool: web::Data<PgPool>,
150 user: AuthUser,
151 params: web::Path<Uuid>,
152) -> ControllerResult<web::Json<ChatbotConversation>> {
153 let token = skip_authorize();
154
155 let mut conn = pool.acquire().await?;
156 let mut tx = conn.begin().await?;
157
158 let configuration = models::chatbot_configurations::get_by_id(&mut tx, *params).await?;
159
160 let conversation = models::chatbot_conversations::insert(
161 &mut tx,
162 ChatbotConversation {
163 id: Uuid::new_v4(),
164 created_at: Utc::now(),
165 updated_at: Utc::now(),
166 deleted_at: None,
167 course_id: configuration.course_id,
168 user_id: user.id,
169 chatbot_configuration_id: configuration.id,
170 },
171 )
172 .await?;
173
174 let _first_message = models::chatbot_conversation_messages::insert(
175 &mut tx,
176 models::chatbot_conversation_messages::ChatbotConversationMessage {
177 id: Uuid::new_v4(),
178 created_at: Utc::now(),
179 updated_at: Utc::now(),
180 deleted_at: None,
181 conversation_id: conversation.id,
182 message: Some(configuration.initial_message.clone()),
183 message_role: MessageRole::Assistant,
184 message_is_complete: true,
185 used_tokens: estimate_tokens(&configuration.initial_message),
186 order_number: 0,
187 tool_output: None,
188 tool_call_fields: vec![],
189 },
190 )
191 .await?;
192
193 tx.commit().await?;
194
195 token.authorized_ok(web::Json(conversation))
196}
197
198#[utoipa::path(
204 get,
205 path = "/{chatbot_configuration_id}/conversations/current",
206 operation_id = "getChatbotCurrentConversationInfo",
207 tag = "course-material-chatbot",
208 params(
209 ("chatbot_configuration_id" = Uuid, Path, description = "Chatbot configuration id")
210 ),
211 responses(
212 (
213 status = 200,
214 description = "Current chatbot conversation info",
215 body = ChatbotConversationInfo
216 )
217 )
218)]
219#[instrument(skip(pool, app_conf))]
220async fn current_conversation_info(
221 pool: web::Data<PgPool>,
222 user: AuthUser,
223 app_conf: web::Data<ApplicationConfiguration>,
224 params: web::Path<Uuid>,
225) -> ControllerResult<web::Json<ChatbotConversationInfo>> {
226 let token = skip_authorize();
227
228 let mut conn = pool.acquire().await?;
229 let chatbot_configuration =
230 models::chatbot_configurations::get_by_id(&mut conn, *params).await?;
231 let res = chatbot_conversations::get_current_conversation_info(
232 &mut conn,
233 user.id,
234 chatbot_configuration.id,
235 )
236 .await?;
237
238 if chatbot_configuration.suggest_next_messages
239 && let Some(suggested_messages) = &res.suggested_messages
241 && suggested_messages.is_empty()
242 && let Some(current_conversation_messages) = &res.current_conversation_messages
243 && let Some(last_message) = current_conversation_messages.last()
244 {
245 let initial_suggested_messages = if last_message.order_number == 0 {
246 let initial_suggested_messages = chatbot_configuration
248 .initial_suggested_messages
249 .unwrap_or(vec![]);
250 if initial_suggested_messages.len() > 3 {
252 let mut rng = rand::rng();
253 initial_suggested_messages
254 .sample(&mut rng, 3)
255 .cloned()
256 .collect()
257 } else {
258 initial_suggested_messages
259 }
260 } else {
261 let course_description =
263 models::courses::get_course(&mut conn, chatbot_configuration.course_id)
264 .await?
265 .description;
266 let message_suggest_llm =
267 models::application_task_default_language_models::get_for_task(
268 &mut conn,
269 ApplicationTask::MessageSuggestion,
270 )
271 .await?;
272 headless_lms_chatbot::message_suggestion::generate_suggested_messages(
273 &app_conf,
274 message_suggest_llm,
275 current_conversation_messages,
276 chatbot_configuration.initial_suggested_messages,
277 &res.course_name,
278 course_description,
279 )
280 .await?
281 };
282
283 if !initial_suggested_messages.is_empty() {
284 headless_lms_models::chatbot_conversation_suggested_messages::insert_batch(
285 &mut conn,
286 &last_message.id,
287 initial_suggested_messages,
288 )
289 .await?;
290 }
291
292 let res = chatbot_conversations::get_current_conversation_info(
293 &mut conn,
294 user.id,
295 chatbot_configuration.id,
296 )
297 .await?;
298 return token.authorized_ok(web::Json(res));
299 }
300
301 token.authorized_ok(web::Json(res))
302}
303
304pub fn _add_routes(cfg: &mut ServiceConfig) {
312 cfg.route(
313 "/{chatbot_configuration_id}/conversations/{conversation_id}/send-message",
314 web::post().to(send_message),
315 )
316 .route(
317 "/{chatbot_configuration_id}/conversations/current",
318 web::get().to(current_conversation_info),
319 )
320 .route(
321 "/{chatbot_configuration_id}/conversations/new",
322 web::post().to(new_conversation),
323 )
324 .route(
325 "/default-for-course/{course_id}",
326 web::get().to(get_default_chatbot_configuration_for_course),
327 );
328}