1#![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
24pub fn input(path: impl Into<String>) -> WorkflowValue {
26 value(WorkflowValueKind::Input(
27 crate::workflow::WorkflowPathSource { path: path.into() },
28 ))
29}
30
31pub fn signal(path: impl Into<String>) -> WorkflowValue {
33 value(WorkflowValueKind::Signal(
34 crate::workflow::WorkflowPathSource { path: path.into() },
35 ))
36}
37
38pub 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
48pub 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
58pub fn literal(literal: Value) -> crate::Result<WorkflowValue> {
60 Ok(value(WorkflowValueKind::Literal(literal)))
61}
62
63pub fn template(template: impl Into<String>) -> WorkflowValue {
65 value(WorkflowValueKind::Template(WorkflowText {
66 template: template.into(),
67 }))
68}
69
70pub fn object(fields: WorkflowMap) -> WorkflowValue {
72 value(WorkflowValueKind::Object(crate::workflow::WorkflowObject {
73 fields,
74 }))
75}
76
77pub 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
93pub 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 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 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
238pub 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
251pub 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 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 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
399pub 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
434pub 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}