Skip to main content

headless_lms_server/controllers/main_frontend/
chatbots.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/chatbots/`.
2use 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/// GET `/api/v0/main-frontend/chatbots/{chatbot_configuration_id}`
24#[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/// POST `/api/v0/main-frontend/chatbots/{chatbot_configuration_id}`
56#[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/// DELETE `/api/v0/main-frontend/chatbots/{chatbot_configuration_id}`
116#[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/// GET `/api/v0/main-frontend/chatbots`
148#[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/// POST `/api/v0/main-frontend/chatbots/create`
169#[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}