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;
13
14use crate::prelude::*;
15
16#[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#[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 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#[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#[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 && 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 let initial_suggested_messages = chatbot_configuration
181 .initial_suggested_messages
182 .unwrap_or(vec![]);
183 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 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
237pub 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}