use super::graph::{EdgeConfig, WorkflowGraph};
use super::node::{RetryPolicy, WorkflowNode};
use super::state::WorkflowValue;
use crate::llm::LLMAgent;
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
pub struct WorkflowBuilder {
graph: WorkflowGraph,
current_node: Option<String>,
}
impl WorkflowBuilder {
pub fn new(id: &str, name: &str) -> Self {
Self {
graph: WorkflowGraph::new(id, name),
current_node: None,
}
}
pub fn description(mut self, desc: &str) -> Self {
self.graph = self.graph.with_description(desc);
self
}
pub fn start(mut self) -> Self {
let node = WorkflowNode::start("start");
self.graph.add_node(node);
self.current_node = Some("start".to_string());
self
}
pub fn start_with_id(mut self, id: &str) -> Self {
let node = WorkflowNode::start(id);
self.graph.add_node(node);
self.current_node = Some(id.to_string());
self
}
pub fn end(mut self) -> Self {
let node = WorkflowNode::end("end");
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, "end");
}
self.current_node = Some("end".to_string());
self
}
pub fn end_with_id(mut self, id: &str) -> Self {
let node = WorkflowNode::end(id);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn task<F, Fut>(mut self, id: &str, name: &str, executor: F) -> Self
where
F: Fn(super::state::WorkflowContext, WorkflowValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<WorkflowValue, String>> + Send + 'static,
{
let node = WorkflowNode::task(id, name, executor);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn task_with_config<F, Fut>(
mut self,
id: &str,
name: &str,
executor: F,
retry: RetryPolicy,
timeout_ms: u64,
) -> Self
where
F: Fn(super::state::WorkflowContext, WorkflowValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<WorkflowValue, String>> + Send + 'static,
{
let node = WorkflowNode::task(id, name, executor)
.with_retry(retry)
.with_timeout(timeout_ms);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn agent<F, Fut>(mut self, id: &str, name: &str, agent_fn: F) -> Self
where
F: Fn(super::state::WorkflowContext, WorkflowValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<WorkflowValue, String>> + Send + 'static,
{
let node = WorkflowNode::agent(id, name, agent_fn);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn llm_agent(mut self, id: &str, name: &str, agent: Arc<LLMAgent>) -> Self {
let node = WorkflowNode::llm_agent(id, name, agent);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn llm_agent_with_template(
mut self,
id: &str,
name: &str,
agent: Arc<LLMAgent>,
prompt_template: String,
) -> Self {
let node = WorkflowNode::llm_agent_with_template(id, name, agent, prompt_template);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn condition<F, Fut>(mut self, id: &str, name: &str, condition_fn: F) -> ConditionBuilder
where
F: Fn(super::state::WorkflowContext, WorkflowValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = bool> + Send + 'static,
{
let node = WorkflowNode::condition(id, name, condition_fn);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
ConditionBuilder {
parent: self,
condition_node: id.to_string(),
true_branch: None,
false_branch: None,
}
}
pub fn parallel(mut self, id: &str, name: &str) -> ParallelBuilder {
let node = WorkflowNode::parallel(id, name, vec![]);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
ParallelBuilder {
parent: self,
parallel_node: id.to_string(),
branches: Vec::new(),
}
}
pub fn loop_node<F, Fut, C, CFut>(
mut self,
id: &str,
name: &str,
body: F,
condition: C,
max_iterations: u32,
) -> Self
where
F: Fn(super::state::WorkflowContext, WorkflowValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<WorkflowValue, String>> + Send + 'static,
C: Fn(super::state::WorkflowContext, WorkflowValue) -> CFut + Send + Sync + 'static,
CFut: Future<Output = bool> + Send + 'static,
{
let node = WorkflowNode::loop_node(id, name, body, condition, max_iterations);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn sub_workflow(mut self, id: &str, name: &str, sub_workflow_id: &str) -> Self {
let node = WorkflowNode::sub_workflow(id, name, sub_workflow_id);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn wait(mut self, id: &str, name: &str, event_type: &str) -> Self {
let node = WorkflowNode::wait(id, name, event_type);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn transform<F, Fut>(mut self, id: &str, name: &str, transform_fn: F) -> Self
where
F: Fn(HashMap<String, WorkflowValue>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = WorkflowValue> + Send + 'static,
{
let node = WorkflowNode::transform(id, name, transform_fn);
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, id);
}
self.current_node = Some(id.to_string());
self
}
pub fn node(mut self, node: WorkflowNode) -> Self {
let node_id = node.id().to_string();
self.graph.add_node(node);
if let Some(ref current) = self.current_node {
self.graph.connect(current, &node_id);
}
self.current_node = Some(node_id);
self
}
pub fn edge(mut self, from: &str, to: &str) -> Self {
self.graph.connect(from, to);
self
}
pub fn conditional_edge(mut self, from: &str, to: &str, condition: &str) -> Self {
self.graph.connect_conditional(from, to, condition);
self
}
pub fn error_edge(mut self, from: &str, to: &str) -> Self {
self.graph.add_edge(EdgeConfig::error(from, to));
self
}
pub fn goto(mut self, node_id: &str) -> Self {
self.current_node = Some(node_id.to_string());
self
}
pub fn then(mut self, node_id: &str) -> Self {
if let Some(ref current) = self.current_node {
self.graph.connect(current, node_id);
}
self.current_node = Some(node_id.to_string());
self
}
pub fn build(self) -> WorkflowGraph {
self.graph
}
pub fn build_validated(self) -> Result<WorkflowGraph, Vec<String>> {
self.graph.validate()?;
Ok(self.graph)
}
}
pub struct ConditionBuilder {
parent: WorkflowBuilder,
condition_node: String,
true_branch: Option<String>,
false_branch: Option<String>,
}
impl ConditionBuilder {
pub fn on_true<F, Fut>(mut self, id: &str, name: &str, executor: F) -> Self
where
F: Fn(super::state::WorkflowContext, WorkflowValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<WorkflowValue, String>> + Send + 'static,
{
let node = WorkflowNode::task(id, name, executor);
self.parent.graph.add_node(node);
self.parent
.graph
.connect_conditional(&self.condition_node, id, "true");
self.true_branch = Some(id.to_string());
self
}
pub fn on_false<F, Fut>(mut self, id: &str, name: &str, executor: F) -> Self
where
F: Fn(super::state::WorkflowContext, WorkflowValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<WorkflowValue, String>> + Send + 'static,
{
let node = WorkflowNode::task(id, name, executor);
self.parent.graph.add_node(node);
self.parent
.graph
.connect_conditional(&self.condition_node, id, "false");
self.false_branch = Some(id.to_string());
self
}
pub fn merge(mut self, id: &str, name: &str) -> WorkflowBuilder {
let node = WorkflowNode::join(
id,
name,
vec![
self.true_branch.as_deref().unwrap_or(""),
self.false_branch.as_deref().unwrap_or(""),
]
.into_iter()
.filter(|s| !s.is_empty())
.collect(),
);
self.parent.graph.add_node(node);
if let Some(ref true_branch) = self.true_branch {
self.parent.graph.connect(true_branch, id);
}
if let Some(ref false_branch) = self.false_branch {
self.parent.graph.connect(false_branch, id);
}
self.parent.current_node = Some(id.to_string());
self.parent
}
pub fn end_condition(mut self) -> WorkflowBuilder {
self.parent.current_node = self.true_branch.or(self.false_branch);
self.parent
}
}
pub struct ParallelBuilder {
parent: WorkflowBuilder,
parallel_node: String,
branches: Vec<String>,
}
impl ParallelBuilder {
pub fn branch<F, Fut>(mut self, id: &str, name: &str, executor: F) -> Self
where
F: Fn(super::state::WorkflowContext, WorkflowValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<WorkflowValue, String>> + Send + 'static,
{
let node = WorkflowNode::task(id, name, executor);
self.parent.graph.add_node(node);
self.parent.graph.connect(&self.parallel_node, id);
self.branches.push(id.to_string());
self
}
pub fn branch_agent<F, Fut>(mut self, id: &str, name: &str, agent_fn: F) -> Self
where
F: Fn(super::state::WorkflowContext, WorkflowValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = Result<WorkflowValue, String>> + Send + 'static,
{
let node = WorkflowNode::agent(id, name, agent_fn);
self.parent.graph.add_node(node);
self.parent.graph.connect(&self.parallel_node, id);
self.branches.push(id.to_string());
self
}
pub fn llm_agent_branch(mut self, id: &str, name: &str, agent: Arc<LLMAgent>) -> Self {
let node = WorkflowNode::llm_agent(id, name, agent);
self.parent.graph.add_node(node);
self.parent.graph.connect(&self.parallel_node, id);
self.branches.push(id.to_string());
self
}
pub fn join(mut self, id: &str, name: &str) -> WorkflowBuilder {
let node = WorkflowNode::join(id, name, self.branches.iter().map(|s| s.as_str()).collect());
self.parent.graph.add_node(node);
for branch in &self.branches {
self.parent.graph.connect(branch, id);
}
self.parent.current_node = Some(id.to_string());
self.parent
}
pub fn join_with_transform<F, Fut>(
mut self,
id: &str,
name: &str,
transform: F,
) -> WorkflowBuilder
where
F: Fn(HashMap<String, WorkflowValue>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = WorkflowValue> + Send + 'static,
{
let node = WorkflowNode::join_with_transform(
id,
name,
self.branches.iter().map(|s| s.as_str()).collect(),
transform,
);
self.parent.graph.add_node(node);
for branch in &self.branches {
self.parent.graph.connect(branch, id);
}
self.parent.current_node = Some(id.to_string());
self.parent
}
}
#[macro_export]
macro_rules! workflow {
($id:expr, $name:expr => {
$($body:tt)*
}) => {
WorkflowBuilder::new($id, $name)
$($body)*
.build()
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_workflow_builder() {
let graph = WorkflowBuilder::new("test", "Test Workflow")
.start()
.task("task1", "Task 1", |_ctx, input| async move { Ok(input) })
.task("task2", "Task 2", |_ctx, input| async move { Ok(input) })
.end()
.build();
assert_eq!(graph.node_count(), 4);
assert_eq!(graph.edge_count(), 3);
}
#[test]
fn test_condition_builder() {
let graph = WorkflowBuilder::new("test", "Conditional Workflow")
.start()
.condition("check", "Check", |_ctx, input| async move {
input.as_i64().unwrap_or(0) > 10
})
.on_true("high", "High", |_ctx, _input| async move {
Ok(WorkflowValue::String("high".to_string()))
})
.on_false("low", "Low", |_ctx, _input| async move {
Ok(WorkflowValue::String("low".to_string()))
})
.merge("merge", "Merge")
.end()
.build();
assert_eq!(graph.node_count(), 6);
}
#[test]
fn test_parallel_builder() {
let graph = WorkflowBuilder::new("test", "Parallel Workflow")
.start()
.parallel("fork", "Fork")
.branch("a", "Branch A", |_ctx, _input| async move {
Ok(WorkflowValue::String("a".to_string()))
})
.branch("b", "Branch B", |_ctx, _input| async move {
Ok(WorkflowValue::String("b".to_string()))
})
.branch("c", "Branch C", |_ctx, _input| async move {
Ok(WorkflowValue::String("c".to_string()))
})
.join("join", "Join")
.end()
.build();
assert_eq!(graph.node_count(), 7);
}
}