headless_lms_server/controllers/main_frontend/
chatbots.rs1use crate::prelude::*;
3use headless_lms_models::{
4 application_task_default_language_models::ApplicationTask,
5 chatbot_configurations::CreateChatbotRequest,
6};
7use utoipa::OpenApi;
8
9use models::chatbot_configurations::{
10 ChatbotConfiguration, NewChatbotConf, normalized_tool_categories,
11};
12
13#[derive(OpenApi)]
14#[openapi(paths(
15 get_chatbot,
16 edit_chatbot,
17 delete_chatbot,
18 get_all_chatbots,
19 create_chatbot
20))]
21pub(crate) struct MainFrontendChatbotsApiDoc;
22
23#[utoipa::path(
25 get,
26 path = "/{chatbot_configuration_id}",
27 operation_id = "getChatbotConfiguration",
28 tag = "chatbots",
29 params(
30 ("chatbot_configuration_id" = Uuid, Path, description = "Chatbot configuration id")
31 ),
32 responses(
33 (status = 200, description = "Chatbot configuration", body = ChatbotConfiguration)
34 )
35)]
36#[instrument(skip(pool))]
37async fn get_chatbot(
38 chatbot_configuration_id: web::Path<Uuid>,
39 pool: web::Data<PgPool>,
40 user: AuthUser,
41) -> ControllerResult<web::Json<ChatbotConfiguration>> {
42 let mut conn = pool.acquire().await?;
43 let configuration =
44 models::chatbot_configurations::get_by_id(&mut conn, *chatbot_configuration_id).await?;
45
46 let token = if let Some(course_id) = configuration.course_id {
47 authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?
48 } else {
49 authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?
50 };
51
52 token.authorized_ok(web::Json(configuration))
53}
54
55#[utoipa::path(
57 post,
58 path = "/{chatbot_configuration_id}",
59 operation_id = "configureChatbot",
60 tag = "chatbots",
61 params(
62 ("chatbot_configuration_id" = Uuid, Path, description = "Chatbot configuration id")
63 ),
64 request_body = NewChatbotConf,
65 responses(
66 (status = 200, description = "Updated chatbot configuration", body = ChatbotConfiguration),
67 (status = 403, description = "Enabling or disabling admin-support tool categories requires global admin permissions")
68 )
69)]
70#[instrument(skip(pool, payload))]
71async fn edit_chatbot(
72 chatbot_configuration_id: web::Path<Uuid>,
73 payload: web::Json<NewChatbotConf>,
74 pool: web::Data<PgPool>,
75 user: AuthUser,
76) -> ControllerResult<web::Json<ChatbotConfiguration>> {
77 let mut conn = pool.acquire().await?;
78 let chatbot =
79 models::chatbot_configurations::get_by_id(&mut conn, *chatbot_configuration_id).await?;
80 let token = if let Some(course_id) = chatbot.course_id {
81 authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?
82 } else {
83 authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?
84 };
85
86 let stored_admin_categories: Vec<_> =
87 normalized_tool_categories(&chatbot.enabled_tool_categories)
88 .into_iter()
89 .filter(|category| category.requires_global_admin())
90 .collect();
91 let requested_admin_categories: Vec<_> =
92 normalized_tool_categories(&payload.enabled_tool_categories)
93 .into_iter()
94 .filter(|category| category.requires_global_admin())
95 .collect();
96 if stored_admin_categories != requested_admin_categories {
97 authorize(
98 &mut conn,
99 Act::Administrate,
100 Some(user.id),
101 Res::GlobalPermissions,
102 )
103 .await?;
104 }
105
106 let configuration: ChatbotConfiguration = models::chatbot_configurations::edit(
107 &mut conn,
108 payload.into_inner(),
109 *chatbot_configuration_id,
110 )
111 .await?;
112 token.authorized_ok(web::Json(configuration))
113}
114
115#[utoipa::path(
117 delete,
118 path = "/{chatbot_configuration_id}",
119 operation_id = "deleteChatbotConfiguration",
120 tag = "chatbots",
121 params(
122 ("chatbot_configuration_id" = Uuid, Path, description = "Chatbot configuration id")
123 ),
124 responses(
125 (status = 200, description = "Deleted chatbot configuration")
126 )
127)]
128#[instrument(skip(pool))]
129async fn delete_chatbot(
130 chatbot_configuration_id: web::Path<Uuid>,
131 pool: web::Data<PgPool>,
132 user: AuthUser,
133) -> ControllerResult<web::Json<()>> {
134 let mut conn = pool.acquire().await?;
135 let chatbot =
136 models::chatbot_configurations::get_by_id(&mut conn, *chatbot_configuration_id).await?;
137 let token = if let Some(course_id) = chatbot.course_id {
138 authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?
139 } else {
140 authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?
141 };
142 models::chatbot_configurations::delete(&mut conn, *chatbot_configuration_id).await?;
143
144 token.authorized_ok(web::Json(()))
145}
146
147#[utoipa::path(
149 get,
150 path = "/",
151 operation_id = "getAllChatbots",
152 tag = "chatbots",
153 responses(
154 (status = 200, description = "All chatbots", body = Vec<ChatbotConfiguration>)
155 )
156)]
157#[instrument(skip(pool))]
158async fn get_all_chatbots(
159 pool: web::Data<PgPool>,
160 user: AuthUser,
161) -> ControllerResult<web::Json<Vec<ChatbotConfiguration>>> {
162 let mut conn = pool.acquire().await?;
163 let all_chatbots = models::chatbot_configurations::get_all_chatbots(&mut conn).await?;
164 let token = authorize(&mut conn, Act::View, Some(user.id), Res::GlobalPermissions).await?;
165 token.authorized_ok(web::Json(all_chatbots))
166}
167
168#[utoipa::path(
170 post,
171 path = "/create",
172 operation_id = "createChatbot",
173 tag = "chatbots",
174 request_body(
175 content = CreateChatbotRequest,
176 description = "JSON object with chatbot name and optional course id, e.g. \"name: 'Chatbot 1', course_id: null, purpose: 'This chatbot will help students learn.'\".",
177 content_type = "application/json"
178 ),
179 responses(
180 (status = 200, description = "Created chatbot", body = ChatbotConfiguration)
181 )
182)]
183#[instrument(skip(pool, payload, app_conf))]
184async fn create_chatbot(
185 payload: web::Json<CreateChatbotRequest>,
186 app_conf: web::Data<ApplicationConfiguration>,
187 pool: web::Data<PgPool>,
188 user: AuthUser,
189) -> ControllerResult<web::Json<ChatbotConfiguration>> {
190 let mut conn = pool.acquire().await?;
191 let course_id = payload.course_id;
192 let purpose = &payload.purpose;
193 let name = &payload.name;
194 if purpose.trim().is_empty() || name.trim().is_empty() {
195 return Err(controller_err!(
196 BadRequest,
197 "Chatbot configuration name or purpose cannot be empty"
198 ));
199 }
200 let (token, course) = if let Some(course_id) = course_id {
201 let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
202 let course = models::courses::get_course(&mut conn, course_id).await?;
203
204 if !course.can_add_chatbot {
205 return Err(controller_err!(
206 BadRequest,
207 "Course doesn't allow creating chatbots.".to_string()
208 ));
209 }
210 (token, Some(course))
211 } else {
212 (
213 authorize(&mut conn, Act::Edit, Some(user.id), Res::GlobalPermissions).await?,
214 None,
215 )
216 };
217
218 let mut tx = conn.begin().await?;
219
220 let model = models::chatbot_configurations_models::get_default(&mut tx)
221 .await
222 .map_err(|e| {
223 controller_err!(
224 BadRequest,
225 "No default chatbot model configured. Ask an admin to set one.".to_string(),
226 e
227 )
228 })?;
229
230 let mut chatbot_to_insert = NewChatbotConf {
231 chatbot_name: name.to_owned(),
232 course_id,
233 model_id: model.id,
234 publicly_accessible: course_id.is_none(),
235 ..Default::default()
236 };
237 if !payload.skip_azure_stuff {
238 let task_llm = models::application_task_default_language_models::get_for_task(
239 &mut tx,
240 ApplicationTask::PromptCreation,
241 )
242 .await?;
243
244 let (course_name, course_desc) = if let Some(c) = course {
245 (Some(c.name), c.description)
246 } else {
247 (None, None)
248 };
249
250 let prompt_res = headless_lms_chatbot::prompt_creation::generate_prompt(
251 app_conf.as_ref(),
252 task_llm.clone(),
253 course_name.to_owned(),
254 course_desc.to_owned(),
255 purpose,
256 )
257 .await?;
258
259 chatbot_to_insert = NewChatbotConf {
260 prompt: prompt_res.prompt,
261 initial_message: prompt_res.first_message,
262 initial_suggested_messages: Some(prompt_res.suggested_messages),
263 ..chatbot_to_insert
264 };
265 };
266
267 let configuration =
268 models::chatbot_configurations::insert(&mut tx, PKeyPolicy::Generate, chatbot_to_insert)
269 .await?;
270 tx.commit().await?;
271
272 token.authorized_ok(web::Json(configuration))
273}
274
275pub fn _add_routes(cfg: &mut web::ServiceConfig) {
276 cfg.route("/{chatbot_configuration_id}", web::get().to(get_chatbot))
277 .route("/create", web::post().to(create_chatbot))
278 .route("/{chatbot_configuration_id}", web::post().to(edit_chatbot))
279 .route(
280 "/{chatbot_configuration_id}",
281 web::delete().to(delete_chatbot),
282 )
283 .route("/", web::get().to(get_all_chatbots));
284}