headless_lms_server/controllers/main_frontend/courses/
chatbots.rs

1//! Controllers for requests starting with `/api/v0/main-frontend/{course_id}/chatbots`.
2use crate::prelude::*;
3
4use headless_lms_models::chatbot_configurations::NewChatbotConf;
5use models::chatbot_configurations::ChatbotConfiguration;
6use utoipa::OpenApi;
7
8#[derive(OpenApi)]
9#[openapi(paths(
10    get_chatbots,
11    create_chatbot,
12    set_default_chatbot,
13    set_non_default_chatbot
14))]
15pub(crate) struct MainFrontendCourseChatbotsApiDoc;
16
17/// GET `/api/v0/main-frontend/courses/{course_id}/chatbots`
18#[utoipa::path(
19    get,
20    path = "",
21    operation_id = "getCourseChatbots",
22    tag = "courses",
23    params(
24        ("course_id" = String, Path, description = "Course id")
25    ),
26    responses(
27        (status = 200, description = "Course chatbots", body = Vec<ChatbotConfiguration>)
28    )
29)]
30#[instrument(skip(pool))]
31async fn get_chatbots(
32    course_id: web::Path<Uuid>,
33    pool: web::Data<PgPool>,
34    user: AuthUser,
35) -> ControllerResult<web::Json<Vec<ChatbotConfiguration>>> {
36    let mut conn = pool.acquire().await?;
37    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
38
39    let configurations =
40        models::chatbot_configurations::get_for_course(&mut conn, *course_id).await?;
41    token.authorized_ok(web::Json(configurations))
42}
43
44/// POST `/api/v0/main-frontend/courses/{course_id}/chatbots`
45#[utoipa::path(
46    post,
47    path = "",
48    operation_id = "createCourseChatbot",
49    tag = "courses",
50    params(
51        ("course_id" = String, Path, description = "Course id")
52    ),
53    request_body(
54        content = String,
55        description = "JSON string literal chatbot name, e.g. \"Chatbot 1\".",
56        content_type = "application/json"
57    ),
58    responses(
59        (status = 200, description = "Created course chatbot", body = ChatbotConfiguration)
60    )
61)]
62#[instrument(skip(pool, payload))]
63async fn create_chatbot(
64    course_id: web::Path<Uuid>,
65    payload: web::Json<String>,
66    pool: web::Data<PgPool>,
67    user: AuthUser,
68) -> ControllerResult<web::Json<ChatbotConfiguration>> {
69    let mut conn = pool.acquire().await?;
70    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(*course_id)).await?;
71    let mut tx = conn.begin().await?;
72
73    let course = models::courses::get_course(&mut tx, *course_id).await?;
74
75    if !course.can_add_chatbot {
76        return Err(ControllerError::new(
77            ControllerErrorType::BadRequest,
78            "Course doesn't allow creating chatbots.".to_string(),
79            None,
80        ));
81    }
82
83    let model = models::chatbot_configurations_models::get_default(&mut tx)
84        .await
85        .map_err(|e| {
86            ControllerError::new(
87                ControllerErrorType::BadRequest,
88                "No default chatbot model configured. Ask an admin to set one.".to_string(),
89                Some(e.into()),
90            )
91        })?;
92
93    let configuration = models::chatbot_configurations::insert(
94        &mut tx,
95        PKeyPolicy::Generate,
96        NewChatbotConf {
97            chatbot_name: payload.into_inner(),
98            course_id: *course_id,
99            model_id: model.id,
100            thinking_model: model.thinking,
101            ..Default::default()
102        },
103    )
104    .await?;
105    tx.commit().await?;
106
107    token.authorized_ok(web::Json(configuration))
108}
109
110/// POST `/api/v0/main-frontend/courses/{course_id}/chatbots/{chatbot_configuration_id}/set-as-default`
111#[utoipa::path(
112    post,
113    path = "/{chatbot_configuration_id}/set-as-default",
114    operation_id = "setCourseChatbotAsDefault",
115    tag = "courses",
116    params(
117        ("course_id" = String, Path, description = "Course id"),
118        ("chatbot_configuration_id" = String, Path, description = "Chatbot configuration id")
119    ),
120    responses(
121        (status = 200, description = "Updated course chatbot", body = ChatbotConfiguration)
122    )
123)]
124#[instrument(skip(pool))]
125async fn set_default_chatbot(
126    ids: web::Path<(Uuid, Uuid)>,
127    pool: web::Data<PgPool>,
128    user: AuthUser,
129) -> ControllerResult<web::Json<ChatbotConfiguration>> {
130    let mut conn = pool.acquire().await?;
131    let (course_id, chatbot_configuration_id) = *ids;
132
133    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
134    let mut tx = conn.begin().await?;
135
136    models::chatbot_configurations::remove_default_chatbot_from_course(&mut tx, course_id).await?;
137
138    let chatbot =
139        models::chatbot_configurations::get_by_id(&mut tx, chatbot_configuration_id).await?;
140
141    if course_id != chatbot.course_id {
142        return Err(ControllerError::new(
143            ControllerErrorType::BadRequest,
144            "Chatbot course id doesn't match the course id provided.".to_string(),
145            None,
146        ));
147    }
148
149    let configuration = models::chatbot_configurations::set_default_chatbot_for_course(
150        &mut tx,
151        chatbot_configuration_id,
152    )
153    .await?;
154    tx.commit().await?;
155
156    token.authorized_ok(web::Json(configuration))
157}
158
159/// POST `/api/v0/main-frontend/courses/{course_id}/chatbots/{chatbot_configuration_id}/set-as-non-default`
160#[utoipa::path(
161    post,
162    path = "/{chatbot_configuration_id}/set-as-non-default",
163    operation_id = "setCourseChatbotAsNonDefault",
164    tag = "courses",
165    params(
166        ("course_id" = String, Path, description = "Course id"),
167        ("chatbot_configuration_id" = String, Path, description = "Chatbot configuration id")
168    ),
169    responses(
170        (status = 200, description = "Updated course chatbot", body = ChatbotConfiguration)
171    )
172)]
173#[instrument(skip(pool))]
174async fn set_non_default_chatbot(
175    ids: web::Path<(Uuid, Uuid)>,
176    pool: web::Data<PgPool>,
177    user: AuthUser,
178) -> ControllerResult<web::Json<ChatbotConfiguration>> {
179    let mut conn = pool.acquire().await?;
180    let (course_id, chatbot_configuration_id) = *ids;
181
182    let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?;
183    let mut tx = conn.begin().await?;
184
185    models::chatbot_configurations::remove_default_chatbot_from_course(&mut tx, course_id).await?;
186
187    let configuration =
188        models::chatbot_configurations::get_by_id(&mut tx, chatbot_configuration_id).await?;
189
190    if course_id != configuration.course_id {
191        return Err(ControllerError::new(
192            ControllerErrorType::BadRequest,
193            "Chatbot course id doesn't match the course id provided.".to_string(),
194            None,
195        ));
196    }
197    tx.commit().await?;
198
199    token.authorized_ok(web::Json(configuration))
200}
201
202pub fn _add_routes(cfg: &mut web::ServiceConfig) {
203    cfg.route("", web::get().to(get_chatbots))
204        .route("", web::post().to(create_chatbot))
205        .route(
206            "/{chatbot_configuration_id}/set-as-default",
207            web::post().to(set_default_chatbot),
208        )
209        .route(
210            "/{chatbot_configuration_id}/set-as-non-default",
211            web::post().to(set_non_default_chatbot),
212        );
213}