headless_lms_server/controllers/cms/
ai_suggestions.rs1use headless_lms_models::application_task_default_language_models::{self, ApplicationTask};
3use headless_lms_models::cms_ai::ParagraphSuggestionAction;
4use utoipa::{OpenApi, ToSchema};
5
6use crate::prelude::*;
7
8#[derive(Debug, Serialize, Deserialize, ToSchema)]
9
10pub struct ParagraphSuggestionMeta {
11 pub tone: Option<String>,
12 pub language: Option<String>,
13 pub setting_type: Option<String>,
14}
15
16#[derive(Debug, Serialize, Deserialize, ToSchema)]
17
18pub struct ParagraphSuggestionContext {
19 pub page_id: Option<Uuid>,
20 pub course_id: Option<Uuid>,
21 pub locale: Option<String>,
22}
23
24#[derive(Debug, Serialize, Deserialize, ToSchema)]
25
26pub struct ParagraphSuggestionRequest {
27 pub action: ParagraphSuggestionAction,
28 pub content: String,
29 pub is_html: bool,
30 pub meta: Option<ParagraphSuggestionMeta>,
31 pub context: Option<ParagraphSuggestionContext>,
32}
33
34#[derive(Serialize, Deserialize, ToSchema)]
35
36pub struct ParagraphSuggestionResponse {
37 pub suggestions: Vec<String>,
38}
39
40#[derive(Debug, Serialize, Deserialize, ToSchema)]
41
42pub struct ChartSpecGenerationRequest {
43 pub prompt: String,
44 pub current_spec: Option<String>,
45 pub data_url: Option<String>,
46 pub data_format: Option<String>,
47 pub data_sample: Option<String>,
48 pub page_id: Option<Uuid>,
49}
50
51#[derive(Serialize, Deserialize, ToSchema)]
52
53pub struct ChartSpecGenerationResponse {
54 pub spec: String,
55}
56
57#[derive(OpenApi)]
58#[openapi(paths(suggest_paragraph, generate_chart_spec))]
59pub(crate) struct CmsAiSuggestionsApiDoc;
60
61#[instrument(skip(pool, app_conf))]
69#[utoipa::path(
70 post,
71 path = "/paragraph",
72 operation_id = "requestParagraphSuggestions",
73 tag = "cms_ai_suggestions",
74 request_body = ParagraphSuggestionRequest,
75 responses(
76 (status = 200, description = "Generated paragraph suggestions", body = ParagraphSuggestionResponse)
77 )
78)]
79async fn suggest_paragraph(
80 pool: web::Data<PgPool>,
81 app_conf: web::Data<ApplicationConfiguration>,
82 user: AuthUser,
83 payload: web::Json<ParagraphSuggestionRequest>,
84) -> ControllerResult<web::Json<ParagraphSuggestionResponse>> {
85 let mut conn = pool.acquire().await?;
86
87 if payload.content.trim().is_empty() {
89 return Err(ControllerError::new(
90 ControllerErrorType::BadRequest,
91 "Paragraph content must not be empty.".to_string(),
92 None,
93 ));
94 }
95
96 let token = if let Some(ParagraphSuggestionContext {
99 page_id: Some(page_id),
100 ..
101 }) = &payload.context
102 {
103 authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?
104 } else {
105 authorize(&mut conn, Act::Teach, Some(user.id), Res::AnyCourse).await?
106 };
107
108 let task_lm = application_task_default_language_models::get_for_task(
109 &mut conn,
110 ApplicationTask::CmsParagraphSuggestion,
111 )
112 .await?;
113
114 let meta = payload.meta.as_ref();
115 let generator_input = headless_lms_chatbot::cms_ai_suggestion::CmsParagraphSuggestionInput {
116 action: payload.action,
117 content: payload.content.clone(),
118 is_html: payload.is_html,
119 meta_tone: meta.and_then(|m| m.tone.clone()),
120 meta_language: meta.and_then(|m| m.language.clone()),
121 meta_setting_type: meta.and_then(|m| m.setting_type.clone()),
122 };
123
124 drop(conn);
126
127 let suggestions = headless_lms_chatbot::cms_ai_suggestion::generate_paragraph_suggestions(
128 &app_conf,
129 task_lm,
130 &generator_input,
131 )
132 .await?;
133
134 token.authorized_ok(web::Json(ParagraphSuggestionResponse { suggestions }))
135}
136
137#[instrument(skip(pool, app_conf, payload, user))]
147#[utoipa::path(
148 post,
149 path = "/chart-spec",
150 operation_id = "requestChartSpecGeneration",
151 tag = "cms_ai_suggestions",
152 request_body = ChartSpecGenerationRequest,
153 responses(
154 (status = 200, description = "Generated Vega-Lite chart specification", body = ChartSpecGenerationResponse)
155 )
156)]
157async fn generate_chart_spec(
158 pool: web::Data<PgPool>,
159 app_conf: web::Data<ApplicationConfiguration>,
160 user: AuthUser,
161 payload: web::Json<ChartSpecGenerationRequest>,
162) -> ControllerResult<web::Json<ChartSpecGenerationResponse>> {
163 if payload.prompt.trim().is_empty() {
164 return Err(controller_err!(
165 BadRequest,
166 "The chart generation prompt must not be empty.".to_string()
167 ));
168 }
169
170 let mut conn = pool.acquire().await?;
171
172 let token = if let Some(page_id) = payload.page_id {
173 authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(page_id)).await?
174 } else {
175 authorize(&mut conn, Act::Teach, Some(user.id), Res::AnyCourse).await?
176 };
177
178 let language = match payload.page_id {
180 Some(page_id) => {
181 let page = models::pages::get_page(&mut conn, page_id).await?;
182 match page.course_id {
183 Some(course_id) => Some(
184 models::courses::get_course(&mut conn, course_id)
185 .await?
186 .language_code,
187 ),
188 None => None,
189 }
190 }
191 None => None,
192 };
193
194 let task_lm = application_task_default_language_models::get_for_task(
195 &mut conn,
196 ApplicationTask::ChartSpecGeneration,
197 )
198 .await?;
199
200 let generator_input = headless_lms_chatbot::chart_spec_generation::ChartSpecGenerationInput {
201 prompt: payload.prompt.clone(),
202 current_spec: payload.current_spec.clone(),
203 data_url: payload.data_url.clone(),
204 data_format: payload.data_format.clone(),
205 data_sample: payload.data_sample.clone(),
206 language,
207 };
208
209 drop(conn);
211
212 let spec = headless_lms_chatbot::chart_spec_generation::generate_chart_spec(
213 &app_conf,
214 task_lm,
215 &generator_input,
216 )
217 .await?;
218
219 token.authorized_ok(web::Json(ChartSpecGenerationResponse { spec }))
220}
221
222pub fn _add_routes(cfg: &mut ServiceConfig) {
230 cfg.route("/paragraph", web::post().to(suggest_paragraph))
231 .route("/chart-spec", web::post().to(generate_chart_spec));
232}