diff --git a/backend/windmill-api/src/jobs.rs b/backend/windmill-api/src/jobs.rs index 62cae0f352..da646e1311 100644 --- a/backend/windmill-api/src/jobs.rs +++ b/backend/windmill-api/src/jobs.rs @@ -3769,6 +3769,7 @@ async fn batch_rerun_handle_job( batch_rerun_compute_js_expression(expr.clone(), job.clone()).await?, ); } + InputTransform::Ai => {} } } diff --git a/backend/windmill-common/src/flows.rs b/backend/windmill-common/src/flows.rs index fcfac158d3..ca1f08e8a2 100644 --- a/backend/windmill-common/src/flows.rs +++ b/backend/windmill-common/src/flows.rs @@ -643,6 +643,7 @@ pub enum InputTransform { #[serde(default = "default_empty_string")] expr: String, }, + Ai, } impl InputTransform { @@ -661,6 +662,7 @@ impl TryFrom for InputTransform { let input_transform = match value.type_.as_str() { "static" => InputTransform::new_static_value(value.value.unwrap_or_else(default_null)), "javascript" => InputTransform::new_javascript_expr(&value.expr.unwrap_or_default()), + "ai" => InputTransform::Ai, other => { return Err(anyhow::anyhow!( "got value: {other} for field `type`, expected value: `static` or `javascript`" @@ -816,8 +818,7 @@ pub struct McpToolValue { pub exclude_tools: Vec, } -fn is_none_or_empty_vec(expr: &Option>) -> bool -{ +fn is_none_or_empty_vec(expr: &Option>) -> bool { expr.is_none() || expr.as_ref().unwrap().is_empty() } diff --git a/backend/windmill-worker/src/ai/tools.rs b/backend/windmill-worker/src/ai/tools.rs index d1280f0053..88469652bd 100644 --- a/backend/windmill-worker/src/ai/tools.rs +++ b/backend/windmill-worker/src/ai/tools.rs @@ -322,11 +322,17 @@ async fn execute_windmill_tool( // Evaluate each input transform and merge with AI-provided args for (key, transform) in input_transforms.iter() { // We skip static empty / null values, those are the one the AI will fill in - if let InputTransform::Static { value } = transform { - let val = value.get().trim(); - if val.is_empty() || val == "null" { + match transform { + InputTransform::Static { value } => { + let val = value.get().trim(); + if val.is_empty() || val == "null" { + continue; + } + } + InputTransform::Ai => { continue; } + _ => (), } let result = evaluate_input_transform::>( transform, diff --git a/backend/windmill-worker/src/ai/utils.rs b/backend/windmill-worker/src/ai/utils.rs index fb27c419ba..947b56f549 100644 --- a/backend/windmill-worker/src/ai/utils.rs +++ b/backend/windmill-worker/src/ai/utils.rs @@ -78,6 +78,7 @@ pub fn filter_schema_by_input_transforms( !val.is_empty() && val != "null" } InputTransform::Javascript { expr } => !expr.trim().is_empty(), + InputTransform::Ai => false, }; if is_completed { Some(key.clone()) diff --git a/backend/windmill-worker/src/worker_flow.rs b/backend/windmill-worker/src/worker_flow.rs index dd34f4dc24..f25e04d564 100644 --- a/backend/windmill-worker/src/worker_flow.rs +++ b/backend/windmill-worker/src/worker_flow.rs @@ -1910,11 +1910,8 @@ pub async fn evaluate_input_transform( by_id: Option<&IdContext>, ) -> error::Result where - T: for<'de> serde::Deserialize<'de> + Send, + T: for<'de> serde::Deserialize<'de> + Send + Default, { - let mut context = HashMap::with_capacity(2); - context.insert("result".to_string(), last_result.clone()); - context.insert("previous_result".to_string(), last_result.clone()); match transform { InputTransform::Static { value } => serde_json::from_str(value.get()).map_err(|e| { Error::ExecutionErr(format!( @@ -1923,6 +1920,9 @@ where )) }), InputTransform::Javascript { expr } => { + let mut context = HashMap::with_capacity(2); + context.insert("result".to_string(), last_result.clone()); + context.insert("previous_result".to_string(), last_result.clone()); let result = eval_timeout( expr.to_string(), context, @@ -1947,6 +1947,7 @@ where )) }) } + InputTransform::Ai => Ok(T::default()), } } @@ -2007,6 +2008,7 @@ async fn transform_input( })?; mapped.insert(key.to_string(), v); } + InputTransform::Ai => (), } } @@ -2441,6 +2443,9 @@ async fn push_next_flow_job( ))); } } + InputTransform::Ai => { + user_groups_required = Vec::new(); + } } } else { user_groups_required = Vec::new(); @@ -4348,6 +4353,11 @@ async fn next_forloop_status( /* Iterator is an InputTransform, evaluate it into an array. */ let itered_raw = match iterator { InputTransform::Static { value } => to_raw_value(value), + InputTransform::Ai => { + return Err(Error::ExecutionErr(format!( + "AI input transform not supported for iterator" + )))? + } InputTransform::Javascript { expr } => { let mut context = HashMap::with_capacity(5); context.insert("result".to_string(), arc_last_job_result.clone()); @@ -4429,6 +4439,11 @@ async fn next_forloop_status( ) .await? } + InputTransform::Ai => { + return Err(Error::ExecutionErr(format!( + "AI input transform not supported for iterator" + )))? + } }; serde_json::from_str::>>(itered_raw.get()).map_err( |not_array| { diff --git a/frontend/src/lib/components/InputTransformForm.svelte b/frontend/src/lib/components/InputTransformForm.svelte index 43e4cbd924..afc0812831 100644 --- a/frontend/src/lib/components/InputTransformForm.svelte +++ b/frontend/src/lib/components/InputTransformForm.svelte @@ -159,7 +159,13 @@ function getPropertyType(arg: InputTransform | any): PropertyType { // For agent tools, if static with undefined/empty value, treat as 'ai', meaning the field will be filled by the AI agent dynamically. - if (isAgentTool && arg?.type === 'static' && arg?.value === undefined) { + if ( + isAgentTool && + ((arg?.type === 'static' && arg?.value === undefined) || arg?.type === 'ai') + ) { + if (arg?.type === 'static') { + arg.type = 'ai' + } return 'ai' } @@ -563,7 +569,7 @@ if (e.detail === 'ai') { // Switch to AI mode: static with no value if (arg) { - arg.type = 'static' + arg.type = 'ai' arg.value = undefined arg.expr = undefined }