diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/mod.rs index aa64143686..e4f9e1ee69 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/mod.rs @@ -1,2 +1,4 @@ pub mod omni_gen_image_generate_handler; +mod request_to_costs; +mod request_to_execution_plan; mod transform_request; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/omni_gen_image_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/omni_gen_image_generate_handler.rs index 8795394dbe..e2f0cc28d7 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/omni_gen_image_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/omni_gen_image_generate_handler.rs @@ -2,14 +2,36 @@ use std::sync::Arc; use actix_web::web::Json; use actix_web::{web, HttpRequest}; -use log::warn; +use log::{error, info, warn}; +use sqlx::Acquire; + use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest; use artcraft_api_defs::omni_gen::generate_response::omni_gen_image_generate_response::OmniGenImageGenerateResponse; +use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType; +use enums::by_table::prompts::prompt_type::PromptType; +use enums::common::generation::common_generation_mode::CommonGenerationMode; +use enums::common::generation_provider::GenerationProvider; +use enums::common::visibility::Visibility; +use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{ + insert_generic_inference_job_for_fal_queue_with_apriori_job_token, + InsertGenericInferenceForFalWithAprioriJobTokenArgs, +}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{ + insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem, +}; +use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error; use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; -use crate::http_server::endpoints::omni_gen::generate::image::transform_request::transform_request; +use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; use crate::state::server_state::ServerState; +use super::request_to_costs::estimate_costs; /// Generate an image using the omni-gen unified endpoint. #[utoipa::path( @@ -26,17 +48,187 @@ use crate::state::server_state::ServerState; ), )] pub async fn omni_gen_image_generate_handler( - _http_request: HttpRequest, + http_request: HttpRequest, request: Json, - _server_state: web::Data>, + server_state: web::Data>, ) -> Result, AdvancedCommonWebError> { - let generate_request = transform_request(&request)?; - - let plan = generate_request.build() - .map_err(|err| { - warn!("Invalid image generation request. Error: {:?}, Request: {:?}", err, generate_request); - AdvancedCommonWebError::from_error(err) - })?; - - todo!() + + payments_error_test(&request.prompt.as_deref().unwrap_or(""))?; + + // ==================== SESSION ==================== // + + let mut mysql_connection = server_state.mysql_pool.acquire().await?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + AdvancedCommonWebError::from_anyhow_error(e) + })?; + + let user_token = match maybe_user_session.as_ref() { + Some(session) => &session.user_token, + None => return Err(AdvancedCommonWebError::NotAuthorized), + }; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // ==================== IDEMPOTENCY ==================== // + + let idempotency_token = request.idempotency_token.as_deref() + .unwrap_or("") + .to_string(); + + if let Err(reason) = validate_idempotency_token_format(&idempotency_token) { + return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + // ==================== COST + BILLING ==================== // + + let cost_estimate = estimate_costs(&request)?; + + let cost = cost_estimate.cost_in_credits.unwrap_or(0); + + info!("Charging wallet: {} credits", cost); + + let apriori_job_token = InferenceJobToken::generate(); + + if cost > 0 { + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + } + + // ==================== EXECUTE GENERATION ==================== // + + // TODO: Execute the generation plan via the router client. + // For now, we record the job as pending without dispatching externally. + let external_job_id = ""; + + // ==================== DB TRANSACTION ==================== // + + let ip_address = get_request_ip(&http_request); + + let mut transaction = mysql_connection + .begin() + .await + .map_err(|err| { + error!("Error starting MySQL transaction: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + // -- Prompt -- + + let generation_mode = if request.image_media_tokens.is_some() { + CommonGenerationMode::Edit + } else { + CommonGenerationMode::Text + }; + + let prompt_result = insert_prompt(InsertPromptArgs { + maybe_apriori_prompt_token: None, + prompt_type: PromptType::ArtcraftApp, + maybe_creator_user_token: Some(user_token), + maybe_model_type: None, + maybe_generation_provider: Some(GenerationProvider::Artcraft), + maybe_positive_prompt: request.prompt.as_deref(), + maybe_negative_prompt: None, + maybe_other_args: None, + maybe_generation_mode: Some(generation_mode), + maybe_aspect_ratio: None, + maybe_resolution: None, + maybe_batch_count: request.image_batch_count.map(|c| c as u8), + maybe_generate_audio: None, + maybe_duration_seconds: None, + creator_ip_address: &ip_address, + mysql_executor: &mut *transaction, + phantom: Default::default(), + }).await; + + let prompt_token = match prompt_result { + Ok(token) => Some(token), + Err(err) => { + warn!("Error inserting prompt: {:?}", err); + None + } + }; + + // -- Prompt context items -- + + if let Some(token) = prompt_token.as_ref() { + let mut context_items = Vec::new(); + + if let Some(ref_tokens) = &request.image_media_tokens { + for media_token in ref_tokens { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::Imgsrc, + }); + } + } + + if !context_items.is_empty() { + if let Err(err) = insert_batch_prompt_context_items(InsertBatchArgs { + prompt_token: token.clone(), + items: context_items, + transaction: &mut transaction, + }).await { + warn!("Error inserting batch prompt context items: {:?}", err); + } + } + } + + // -- Inference job -- + + let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token( + InsertGenericInferenceForFalWithAprioriJobTokenArgs { + apriori_job_token: &apriori_job_token, + uuid_idempotency_token: &idempotency_token, + maybe_external_third_party_id: external_job_id, + fal_category: FalCategory::ImageGeneration, + maybe_inference_args: None, + maybe_prompt_token: prompt_token.as_ref(), + maybe_creator_user_token: Some(user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_executor: &mut *transaction, + starting_job_status_override: None, + maybe_frontend_failure_category: None, + maybe_failure_reason: None, + phantom: Default::default(), + } + ).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting inference job: {:?}", err); + return Err(AdvancedCommonWebError::from_error(err)); + } + }; + + transaction.commit().await.map_err(|err| { + error!("Error committing transaction: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + Ok(Json(OmniGenImageGenerateResponse { + success: true, + inference_job_token: job_token, + })) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/request_to_costs.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/request_to_costs.rs new file mode 100644 index 0000000000..23559a49a8 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/request_to_costs.rs @@ -0,0 +1,24 @@ +use artcraft_router::api::provider::Provider; +use artcraft_router::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use log::warn; + +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::endpoints::omni_gen::generate::image::transform_request::transform_request; + +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest; + +/// Build the Artcraft-provider cost plan for this request. +pub fn estimate_costs( + request: &OmniGenImageCostAndGenerateRequest, +) -> Result { + let mut generate_request = transform_request(request)?; + generate_request.provider = Provider::Artcraft; + + let plan = generate_request.build() + .map_err(|e| { + warn!("Failed to build cost plan: {}", e); + AdvancedCommonWebError::from_error(e) + })?; + + Ok(plan.estimate_costs()) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/request_to_execution_plan.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/request_to_execution_plan.rs new file mode 100644 index 0000000000..e88ec358e7 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/request_to_execution_plan.rs @@ -0,0 +1,18 @@ +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::endpoints::omni_gen::generate::image::transform_request::transform_request; + +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest; + +/// Validate that an execution plan can be built for this request. +/// Returns an error if the model/provider combination is unsupported. +pub fn validate_execution_plan( + request: &OmniGenImageCostAndGenerateRequest, +) -> Result<(), AdvancedCommonWebError> { + let generate_request = transform_request(request)?; + + generate_request.build() + .map(|_| ()) + .map_err(|e| AdvancedCommonWebError::BadInputWithSimpleMessage( + format!("Failed to build execution plan: {}", e), + )) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/mod.rs index c3f88f90a4..045d394127 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/mod.rs @@ -1,2 +1,4 @@ pub mod omni_gen_video_generate_handler; +mod request_to_costs; +mod request_to_execution_plan; mod transform_request; diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs index cc446cbd79..e41909cc43 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/omni_gen_video_generate_handler.rs @@ -2,14 +2,37 @@ use std::sync::Arc; use actix_web::web::Json; use actix_web::{web, HttpRequest}; -use log::warn; +use log::{error, info, warn}; +use sqlx::Acquire; + use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest; use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::OmniGenVideoGenerateResponse; +use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType; +use enums::by_table::prompts::prompt_type::PromptType; +use enums::common::generation::common_generation_mode::CommonGenerationMode; +use enums::common::generation_provider::GenerationProvider; +use enums::common::visibility::Visibility; +use http_server_common::request::get_request_ip::get_request_ip; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue_with_apriori_job_token::{ + insert_generic_inference_job_for_fal_queue_with_apriori_job_token, + InsertGenericInferenceForFalWithAprioriJobTokenArgs, +}; +use mysql_queries::queries::generic_inference::fal::insert_generic_inference_job_for_fal_queue::FalCategory; +use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token; +use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{ + insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem, +}; +use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs}; +use tokens::tokens::generic_inference_jobs::InferenceJobToken; +use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error; use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; -use crate::http_server::endpoints::omni_gen::generate::video::transform_request::transform_request; +use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test; +use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; use crate::state::server_state::ServerState; +use super::request_to_costs::estimate_costs; + /// Generate a video using the omni-gen unified endpoint. #[utoipa::path( post, @@ -25,17 +48,210 @@ use crate::state::server_state::ServerState; ), )] pub async fn omni_gen_video_generate_handler( - _http_request: HttpRequest, + http_request: HttpRequest, request: Json, - _server_state: web::Data>, + server_state: web::Data>, ) -> Result, AdvancedCommonWebError> { - let generate_request = transform_request(&request)?; - let plan = generate_request.build() - .map_err(|err| { - warn!("Invalid video generation request. Error: {:?}, Request: {:?}", err, generate_request); - AdvancedCommonWebError::from_error(err) - })?; + payments_error_test(&request.prompt.as_deref().unwrap_or(""))?; - todo!() + // ==================== SESSION ==================== // + + let mut mysql_connection = server_state.mysql_pool.acquire().await?; + + let maybe_user_session = server_state + .session_checker + .maybe_get_user_session_from_connection(&http_request, &mut mysql_connection) + .await + .map_err(|e| { + warn!("Session checker error: {:?}", e); + AdvancedCommonWebError::from_anyhow_error(e) + })?; + + let user_token = match maybe_user_session.as_ref() { + Some(session) => &session.user_token, + None => return Err(AdvancedCommonWebError::NotAuthorized), + }; + + let maybe_avt_token = server_state + .avt_cookie_manager + .get_avt_token_from_request(&http_request); + + // ==================== IDEMPOTENCY ==================== // + + let idempotency_token = request.idempotency_token.as_deref() + .unwrap_or("") + .to_string(); + + if let Err(reason) = validate_idempotency_token_format(&idempotency_token) { + return Err(AdvancedCommonWebError::BadInputWithSimpleMessage(reason)); + } + + insert_idempotency_token(&idempotency_token, &mut *mysql_connection) + .await + .map_err(|err| { + error!("Error inserting idempotency token: {:?}", err); + AdvancedCommonWebError::BadInputWithSimpleMessage("repeated idempotency token".to_string()) + })?; + + // ==================== COST + BILLING ==================== // + + let cost_estimate = estimate_costs(&request)?; + + let cost = cost_estimate.cost_in_credits.unwrap_or(0); + + info!("Charging wallet: {} credits", cost); + + let apriori_job_token = InferenceJobToken::generate(); + + if cost > 0 { + attempt_wallet_deduction_else_common_web_error( + user_token, + Some(apriori_job_token.as_str()), + cost, + &mut mysql_connection, + ).await?; + } + + // ==================== EXECUTE GENERATION ==================== // + + // TODO: Execute the generation plan via the router client. + // For now, we record the job as pending without dispatching externally. + let external_job_id = ""; + + // ==================== DB TRANSACTION ==================== // + + let ip_address = get_request_ip(&http_request); + + let mut transaction = mysql_connection + .begin() + .await + .map_err(|err| { + error!("Error starting MySQL transaction: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + // -- Prompt -- + + let generation_mode = if request.start_frame_image_media_token.is_some() { + CommonGenerationMode::Keyframe + } else { + CommonGenerationMode::Text + }; + + let prompt_result = insert_prompt(InsertPromptArgs { + maybe_apriori_prompt_token: None, + prompt_type: PromptType::ArtcraftApp, + maybe_creator_user_token: Some(user_token), + maybe_model_type: None, + maybe_generation_provider: Some(GenerationProvider::Artcraft), + maybe_positive_prompt: request.prompt.as_deref(), + maybe_negative_prompt: request.negative_prompt.as_deref(), + maybe_other_args: None, + maybe_generation_mode: Some(generation_mode), + maybe_aspect_ratio: None, + maybe_resolution: None, + maybe_batch_count: request.video_batch_count.map(|c| c as u8), + maybe_generate_audio: request.generate_audio, + maybe_duration_seconds: request.duration_seconds.map(|d| d as u32), + creator_ip_address: &ip_address, + mysql_executor: &mut *transaction, + phantom: Default::default(), + }).await; + + let prompt_token = match prompt_result { + Ok(token) => Some(token), + Err(err) => { + warn!("Error inserting prompt: {:?}", err); + None + } + }; + + // -- Prompt context items -- + + if let Some(token) = prompt_token.as_ref() { + let mut context_items = Vec::new(); + + if let Some(media_token) = &request.start_frame_image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::VidStartFrame, + }); + } + + if let Some(media_token) = &request.end_frame_image_media_token { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::VidEndFrame, + }); + } + + if let Some(ref_tokens) = &request.reference_image_media_tokens { + for media_token in ref_tokens { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::Imgref, + }); + } + } + + if let Some(ref_tokens) = &request.reference_video_media_tokens { + for media_token in ref_tokens { + context_items.push(PromptContextItem { + media_token: media_token.clone(), + context_semantic_type: PromptContextSemanticType::VidRef, + }); + } + } + + if !context_items.is_empty() { + if let Err(err) = insert_batch_prompt_context_items(InsertBatchArgs { + prompt_token: token.clone(), + items: context_items, + transaction: &mut transaction, + }).await { + warn!("Error inserting batch prompt context items: {:?}", err); + } + } + } + + // -- Inference job -- + + let db_result = insert_generic_inference_job_for_fal_queue_with_apriori_job_token( + InsertGenericInferenceForFalWithAprioriJobTokenArgs { + apriori_job_token: &apriori_job_token, + uuid_idempotency_token: &idempotency_token, + maybe_external_third_party_id: external_job_id, + fal_category: FalCategory::VideoGeneration, + maybe_inference_args: None, + maybe_prompt_token: prompt_token.as_ref(), + maybe_creator_user_token: Some(user_token), + maybe_avt_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility: Visibility::Public, + mysql_executor: &mut *transaction, + starting_job_status_override: None, + maybe_frontend_failure_category: None, + maybe_failure_reason: None, + phantom: Default::default(), + } + ).await; + + let job_token = match db_result { + Ok(token) => token, + Err(err) => { + warn!("Error inserting inference job: {:?}", err); + return Err(AdvancedCommonWebError::from_error(err)); + } + }; + + transaction.commit().await.map_err(|err| { + error!("Error committing transaction: {:?}", err); + AdvancedCommonWebError::from_error(err) + })?; + + Ok(Json(OmniGenVideoGenerateResponse { + success: true, + inference_job_token: job_token, + })) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_costs.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_costs.rs new file mode 100644 index 0000000000..7c151b0d3a --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_costs.rs @@ -0,0 +1,24 @@ +use artcraft_router::api::provider::Provider; +use artcraft_router::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; +use log::warn; + +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::endpoints::omni_gen::generate::video::transform_request::transform_request; + +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest; + +/// Build the Artcraft-provider cost plan for this request. +pub fn estimate_costs( + request: &OmniGenVideoCostAndGenerateRequest, +) -> Result { + let mut generate_request = transform_request(request)?; + generate_request.provider = Provider::Artcraft; + + let plan = generate_request.build() + .map_err(|e| { + warn!("Failed to build cost plan: {}", e); + AdvancedCommonWebError::from_error(e) + })?; + + Ok(plan.estimate_costs()) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_execution_plan.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_execution_plan.rs new file mode 100644 index 0000000000..48bf020d52 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/request_to_execution_plan.rs @@ -0,0 +1,18 @@ +use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; +use crate::http_server::endpoints::omni_gen::generate::video::transform_request::transform_request; + +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest; + +/// Validate that an execution plan can be built for this request. +/// Returns an error if the model/provider combination is unsupported. +pub fn validate_execution_plan( + request: &OmniGenVideoCostAndGenerateRequest, +) -> Result<(), AdvancedCommonWebError> { + let generate_request = transform_request(request)?; + + generate_request.build() + .map(|_| ()) + .map_err(|e| AdvancedCommonWebError::BadInputWithSimpleMessage( + format!("Failed to build execution plan: {}", e), + )) +}