headless_lms_server/controllers/cms/
ai_suggestions.rs

1//! Controllers for requests starting with `/api/v0/cms/ai-suggestions`.
2use headless_lms_models::application_task_default_language_models::{self, ApplicationTask};
3use headless_lms_models::cms_ai::ParagraphSuggestionAction;
4
5use crate::prelude::*;
6
7#[derive(Debug, Serialize, Deserialize)]
8#[cfg_attr(feature = "ts_rs", derive(TS))]
9pub struct ParagraphSuggestionMeta {
10    pub tone: Option<String>,
11    pub language: Option<String>,
12    pub setting_type: Option<String>,
13}
14
15#[derive(Debug, Serialize, Deserialize)]
16#[cfg_attr(feature = "ts_rs", derive(TS))]
17pub struct ParagraphSuggestionContext {
18    pub page_id: Option<Uuid>,
19    pub course_id: Option<Uuid>,
20    pub locale: Option<String>,
21}
22
23#[derive(Debug, Serialize, Deserialize)]
24#[cfg_attr(feature = "ts_rs", derive(TS))]
25pub struct ParagraphSuggestionRequest {
26    pub action: ParagraphSuggestionAction,
27    pub content: String,
28    pub is_html: bool,
29    pub meta: Option<ParagraphSuggestionMeta>,
30    pub context: Option<ParagraphSuggestionContext>,
31}
32
33#[derive(Serialize, Deserialize)]
34#[cfg_attr(feature = "ts_rs", derive(TS))]
35pub struct ParagraphSuggestionResponse {
36    pub suggestions: Vec<String>,
37}
38
39/**
40POST `/api/v0/cms/ai-suggestions/paragraph` - Generate AI suggestions for a CMS paragraph.
41
42This endpoint is intended for CMS editors. It requires the user to have edit access
43to the referenced page when `context.page_id` is provided, otherwise it falls back
44to requiring a teaching role for some course via `Res::AnyCourse`.
45*/
46#[instrument(skip(pool, app_conf))]
47async fn suggest_paragraph(
48    pool: web::Data<PgPool>,
49    app_conf: web::Data<ApplicationConfiguration>,
50    user: AuthUser,
51    payload: web::Json<ParagraphSuggestionRequest>,
52) -> ControllerResult<web::Json<ParagraphSuggestionResponse>> {
53    let mut conn = pool.acquire().await?;
54
55    // Basic validation of input content.
56    if payload.content.trim().is_empty() {
57        return Err(ControllerError::new(
58            ControllerErrorType::BadRequest,
59            "Paragraph content must not be empty.".to_string(),
60            None,
61        ));
62    }
63
64    // Authorize: prefer page-level edit permission when page_id is available,
65    // otherwise require that the user can teach at least one course.
66    let token = if let Some(ParagraphSuggestionContext {
67        page_id: Some(page_id),
68        ..
69    }) = &payload.context
70    {
71        authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?
72    } else {
73        authorize(&mut conn, Act::Teach, Some(user.id), Res::AnyCourse).await?
74    };
75
76    let task_lm = application_task_default_language_models::get_for_task(
77        &mut conn,
78        ApplicationTask::CmsParagraphSuggestion,
79    )
80    .await?;
81
82    let meta = payload.meta.as_ref();
83    let generator_input = headless_lms_chatbot::cms_ai_suggestion::CmsParagraphSuggestionInput {
84        action: payload.action,
85        content: payload.content.clone(),
86        is_html: payload.is_html,
87        meta_tone: meta.and_then(|m| m.tone.clone()),
88        meta_language: meta.and_then(|m| m.language.clone()),
89        meta_setting_type: meta.and_then(|m| m.setting_type.clone()),
90    };
91
92    // Return the DB connection to the pool before the LLM call.
93    drop(conn);
94
95    let suggestions = headless_lms_chatbot::cms_ai_suggestion::generate_paragraph_suggestions(
96        &app_conf,
97        task_lm,
98        &generator_input,
99    )
100    .await?;
101
102    token.authorized_ok(web::Json(ParagraphSuggestionResponse { suggestions }))
103}
104
105/**
106Add a route for each controller in this module.
107
108The name starts with an underline in order to appear before other functions in the module documentation.
109
110We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation.
111*/
112pub fn _add_routes(cfg: &mut ServiceConfig) {
113    cfg.route("/paragraph", web::post().to(suggest_paragraph));
114}