Skip to main content

gestalt/
workflow_authoring.rs

1//! Typed workflow-definition authoring helpers.
2#![allow(missing_docs)]
3
4use std::collections::BTreeMap;
5
6use serde_json::{Map, Value};
7
8use crate::agent::AgentOutput;
9use crate::app::AgentToolRef;
10use crate::workflow::{
11    BoundWorkflowTarget, WorkflowActivation, WorkflowActivationTrigger, WorkflowAgentMessage,
12    WorkflowDefinitionSpec, WorkflowEventActivation, WorkflowEventMatch,
13    WorkflowScheduleActivation, WorkflowStep, WorkflowStepAction, WorkflowStepAgentTurn,
14    WorkflowStepAppCall, WorkflowStepWhen, WorkflowText, WorkflowValue, WorkflowValueKind,
15};
16
17type WorkflowMap = BTreeMap<String, WorkflowValue>;
18type WorkflowMapFn = Box<dyn Fn() -> WorkflowMap + Send + Sync>;
19
20fn value(kind: WorkflowValueKind) -> WorkflowValue {
21    WorkflowValue { kind: Some(kind) }
22}
23
24/// References a run-input path.
25pub fn input(path: impl Into<String>) -> WorkflowValue {
26    value(WorkflowValueKind::Input(
27        crate::workflow::WorkflowPathSource { path: path.into() },
28    ))
29}
30
31/// References an activation signal path.
32pub fn signal(path: impl Into<String>) -> WorkflowValue {
33    value(WorkflowValueKind::Signal(
34        crate::workflow::WorkflowPathSource { path: path.into() },
35    ))
36}
37
38/// References a prior step output path.
39pub fn step_output(step_id: impl Into<String>, path: impl Into<String>) -> WorkflowValue {
40    value(WorkflowValueKind::StepOutput(
41        crate::workflow::WorkflowStepOutputSource {
42            step_id: step_id.into(),
43            path: path.into(),
44        },
45    ))
46}
47
48/// References a prior step input path.
49pub fn step_input(step_id: impl Into<String>, path: impl Into<String>) -> WorkflowValue {
50    value(WorkflowValueKind::StepInput(
51        crate::workflow::WorkflowStepInputSource {
52            step_id: step_id.into(),
53            path: path.into(),
54        },
55    ))
56}
57
58/// Builds a literal workflow value.
59pub fn literal(literal: Value) -> crate::Result<WorkflowValue> {
60    Ok(value(WorkflowValueKind::Literal(literal)))
61}
62
63/// Builds a workflow template value.
64pub fn template(template: impl Into<String>) -> WorkflowValue {
65    value(WorkflowValueKind::Template(WorkflowText {
66        template: template.into(),
67    }))
68}
69
70/// Builds an object workflow value.
71pub fn object(fields: WorkflowMap) -> WorkflowValue {
72    value(WorkflowValueKind::Object(crate::workflow::WorkflowObject {
73        fields,
74    }))
75}
76
77/// Builds an array workflow value.
78pub fn array(values: Vec<WorkflowValue>) -> WorkflowValue {
79    value(WorkflowValueKind::Array(crate::workflow::WorkflowArray {
80        values,
81    }))
82}
83
84#[derive(Clone, Debug)]
85pub struct WorkflowBuilder {
86    id: String,
87    run_as: String,
88    paused: bool,
89    activations: Vec<WorkflowActivation>,
90    steps: Vec<WorkflowStep>,
91}
92
93/// Starts a fluent workflow definition builder.
94pub fn define_workflow(
95    id: impl Into<String>,
96    run_as: impl Into<String>,
97) -> crate::Result<WorkflowBuilder> {
98    let id = id.into();
99    let run_as = run_as.into();
100    if id.trim().is_empty() {
101        return Err(crate::Error::bad_request("define_workflow requires id"));
102    }
103    if run_as.trim().is_empty() {
104        return Err(crate::Error::bad_request("define_workflow requires run_as"));
105    }
106    Ok(WorkflowBuilder {
107        id,
108        run_as,
109        paused: false,
110        activations: Vec::new(),
111        steps: Vec::new(),
112    })
113}
114
115impl WorkflowBuilder {
116    /// Sets whether the definition starts paused.
117    pub fn paused(mut self, paused: bool) -> Self {
118        self.paused = paused;
119        self
120    }
121
122    pub fn on(mut self, activation: WorkflowActivationConfig) -> Self {
123        let (id, paused, trigger, input) = match activation.kind {
124            WorkflowActivationKind::Event {
125                type_name,
126                map_input,
127            } => (
128                type_name.clone(),
129                false,
130                WorkflowActivationTrigger::Event(WorkflowEventActivation {
131                    r#match: Some(WorkflowEventMatch {
132                        r#type: type_name,
133                        ..Default::default()
134                    }),
135                }),
136                Some(object(map_input())),
137            ),
138            WorkflowActivationKind::Schedule { cron, map_input } => (
139                cron.clone(),
140                false,
141                WorkflowActivationTrigger::Schedule(WorkflowScheduleActivation {
142                    cron,
143                    ..Default::default()
144                }),
145                Some(object(map_input())),
146            ),
147        };
148        self.activations.push(WorkflowActivation {
149            id,
150            paused,
151            trigger: Some(trigger),
152            input,
153        });
154        self
155    }
156
157    pub fn step(mut self, step_id: impl Into<String>, config: WorkflowStepConfig) -> Self {
158        assert!(
159            !(config.app.is_some() && config.agent.is_some()),
160            "workflow step cannot configure both app and agent actions"
161        );
162        let mut step = WorkflowStep {
163            id: step_id.into(),
164            ..Default::default()
165        };
166        if let Some(inputs) = config.inputs {
167            step.inputs = inputs();
168        }
169        if let Some(app) = config.app {
170            step.action = Some(WorkflowStepAction::App(WorkflowStepAppCall {
171                name: app.name,
172                operation: app.operation,
173                input: app.input.map(|callback| object(callback())),
174                connection: app.connection,
175                instance: app.instance,
176                credential_mode: app.credential_mode,
177            }));
178        }
179        if let Some(agent) = config.agent {
180            step.action = Some(WorkflowStepAction::Agent(WorkflowStepAgentTurn {
181                provider: agent.provider,
182                model: agent.model,
183                session_key: agent.session_key,
184                prompt: Some(agent.prompt),
185                messages: agent
186                    .messages
187                    .into_iter()
188                    .map(|message| WorkflowAgentMessage {
189                        role: message.role,
190                        text: Some(WorkflowText {
191                            template: message.text,
192                        }),
193                        ..Default::default()
194                    })
195                    .collect(),
196                tools: agent.tools,
197                output: agent.output,
198                model_options: agent.model_options,
199            }));
200        }
201        step.when = config.when.map(|when| WorkflowStepWhen {
202            value: Some(when.value),
203            equals: when.equals,
204        });
205        step.timeout_seconds = config.timeout_seconds;
206        step.metadata = config.metadata;
207        self.steps.push(step);
208        self
209    }
210
211    /// Consumes the builder and returns the generated workflow definition spec.
212    pub fn to_spec(self) -> WorkflowDefinitionSpec {
213        WorkflowDefinitionSpec {
214            id: self.id,
215            run_as: self.run_as,
216            paused: self.paused,
217            activations: self.activations,
218            target: (!self.steps.is_empty()).then_some(BoundWorkflowTarget { steps: self.steps }),
219        }
220    }
221}
222
223pub struct WorkflowActivationConfig {
224    kind: WorkflowActivationKind,
225}
226
227enum WorkflowActivationKind {
228    Event {
229        type_name: String,
230        map_input: WorkflowMapFn,
231    },
232    Schedule {
233        cron: String,
234        map_input: WorkflowMapFn,
235    },
236}
237
238/// Creates an event activation for [`WorkflowBuilder::on`].
239pub fn event<F>(type_name: impl Into<String>, map_input: F) -> WorkflowActivationConfig
240where
241    F: Fn() -> BTreeMap<String, WorkflowValue> + Send + Sync + 'static,
242{
243    WorkflowActivationConfig {
244        kind: WorkflowActivationKind::Event {
245            type_name: type_name.into(),
246            map_input: Box::new(map_input),
247        },
248    }
249}
250
251/// Creates a schedule activation for [`WorkflowBuilder::on`].
252pub fn schedule<F>(cron: impl Into<String>, map_input: F) -> WorkflowActivationConfig
253where
254    F: Fn() -> BTreeMap<String, WorkflowValue> + Send + Sync + 'static,
255{
256    WorkflowActivationConfig {
257        kind: WorkflowActivationKind::Schedule {
258            cron: cron.into(),
259            map_input: Box::new(map_input),
260        },
261    }
262}
263
264pub struct WorkflowStepAppConfig {
265    pub name: String,
266    pub operation: String,
267    input: Option<WorkflowMapFn>,
268    pub connection: String,
269    pub instance: String,
270    pub credential_mode: String,
271}
272
273impl WorkflowStepAppConfig {
274    /// Creates an app step configuration. Add mapped input with [`Self::with_input`].
275    pub fn new(name: impl Into<String>, operation: impl Into<String>) -> Self {
276        Self {
277            name: name.into(),
278            operation: operation.into(),
279            input: None,
280            connection: String::new(),
281            instance: String::new(),
282            credential_mode: String::new(),
283        }
284    }
285
286    /// Sets a zero-argument input mapper; the closure is boxed by the SDK.
287    pub fn with_input<F>(mut self, input: F) -> Self
288    where
289        F: Fn() -> BTreeMap<String, WorkflowValue> + Send + Sync + 'static,
290    {
291        self.input = Some(Box::new(input));
292        self
293    }
294}
295
296impl Default for WorkflowStepAppConfig {
297    fn default() -> Self {
298        Self::new("", "")
299    }
300}
301
302impl WorkflowStepAppConfig {
303    pub fn with_connection(mut self, connection: impl Into<String>) -> Self {
304        self.connection = connection.into();
305        self
306    }
307
308    pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
309        self.instance = instance.into();
310        self
311    }
312
313    pub fn with_credential_mode(mut self, credential_mode: impl Into<String>) -> Self {
314        self.credential_mode = credential_mode.into();
315        self
316    }
317}
318
319#[derive(Clone)]
320pub struct WorkflowStepAgentMessageConfig {
321    pub role: String,
322    pub text: String,
323}
324
325#[derive(Clone, Default)]
326pub struct WorkflowStepAgentConfig {
327    pub provider: String,
328    pub model: String,
329    pub session_key: String,
330    pub prompt: WorkflowText,
331    pub messages: Vec<WorkflowStepAgentMessageConfig>,
332    pub tools: Vec<AgentToolRef>,
333    pub output: Option<AgentOutput>,
334    pub model_options: Option<Map<String, Value>>,
335}
336
337impl WorkflowStepAgentConfig {
338    pub fn new(provider: impl Into<String>) -> Self {
339        Self {
340            provider: provider.into(),
341            ..Default::default()
342        }
343    }
344
345    pub fn with_prompt(mut self, prompt: WorkflowText) -> Self {
346        self.prompt = prompt;
347        self
348    }
349}
350
351#[derive(Clone)]
352pub struct WorkflowStepWhenConfig {
353    pub value: WorkflowValue,
354    pub equals: Option<Value>,
355}
356
357#[derive(Default)]
358pub struct WorkflowStepConfig {
359    inputs: Option<WorkflowMapFn>,
360    pub app: Option<WorkflowStepAppConfig>,
361    pub agent: Option<WorkflowStepAgentConfig>,
362    pub when: Option<WorkflowStepWhenConfig>,
363    pub timeout_seconds: i32,
364    pub metadata: Option<Map<String, Value>>,
365}
366
367impl WorkflowStepConfig {
368    pub fn with_inputs<F>(mut self, inputs: F) -> Self
369    where
370        F: Fn() -> BTreeMap<String, WorkflowValue> + Send + Sync + 'static,
371    {
372        self.inputs = Some(Box::new(inputs));
373        self
374    }
375}
376
377fn value_placeholder(value: &WorkflowValue) -> crate::Result<String> {
378    let result = match value.kind.as_ref() {
379        Some(WorkflowValueKind::Input(source)) => format!("${{{{ input.{} }}}}", source.path),
380        Some(WorkflowValueKind::Signal(source)) => format!("${{{{ signal.{} }}}}", source.path),
381        Some(WorkflowValueKind::StepOutput(source)) => {
382            format!(
383                "${{{{ steps.{}.outputs.{} }}}}",
384                source.step_id, source.path
385            )
386        }
387        Some(WorkflowValueKind::StepInput(source)) => {
388            format!("${{{{ steps.{}.inputs.{} }}}}", source.step_id, source.path)
389        }
390        _ => {
391            return Err(crate::Error::bad_request(
392                "text references must be path values",
393            ));
394        }
395    };
396    Ok(result)
397}
398
399/// Composes workflow prompt text from workflow value references.
400pub fn text(parts: &[WorkflowValue]) -> crate::Result<WorkflowText> {
401    let mut template = String::new();
402    for part in parts {
403        template.push_str(&value_placeholder(part)?);
404    }
405    Ok(WorkflowText { template })
406}
407
408pub enum WorkflowDefinitionSpecOrBuilder {
409    Spec(WorkflowDefinitionSpec),
410    Builder(WorkflowBuilder),
411}
412
413impl From<WorkflowBuilder> for WorkflowDefinitionSpecOrBuilder {
414    fn from(builder: WorkflowBuilder) -> Self {
415        Self::Builder(builder)
416    }
417}
418
419impl From<WorkflowDefinitionSpec> for WorkflowDefinitionSpecOrBuilder {
420    fn from(spec: WorkflowDefinitionSpec) -> Self {
421        Self::Spec(spec)
422    }
423}
424
425impl WorkflowDefinitionSpecOrBuilder {
426    fn into_spec(self) -> WorkflowDefinitionSpec {
427        match self {
428            Self::Spec(spec) => spec,
429            Self::Builder(builder) => builder.to_spec(),
430        }
431    }
432}
433
434/// Applies a workflow definition from a raw spec or an authored builder.
435pub async fn apply_workflow_definition(
436    workflow: &mut crate::workflow::Workflow,
437    provider: String,
438    idempotency_key: String,
439    spec: Option<impl Into<WorkflowDefinitionSpecOrBuilder>>,
440) -> Result<crate::workflow::WorkflowDefinition, crate::rpc_support::GestaltError> {
441    workflow
442        .apply_definition(
443            provider,
444            idempotency_key,
445            spec.map(|value| value.into().into_spec()),
446        )
447        .await
448}