use super::agent::LLMAgent;
use super::types::{LLMError, LLMResult};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub enum AgentNodeType {
Start,
End,
Agent,
Router,
Parallel,
Join,
Transform,
}
#[derive(Debug, Clone)]
pub enum AgentValue {
Null,
Text(String),
Texts(Vec<String>),
Map(HashMap<String, String>),
Json(serde_json::Value),
}
impl AgentValue {
pub fn as_text(&self) -> Option<&str> {
match self {
AgentValue::Text(s) => Some(s),
_ => None,
}
}
pub fn into_text(self) -> String {
match self {
AgentValue::Text(s) => s,
AgentValue::Texts(v) => v.join("\n"),
AgentValue::Map(m) => serde_json::to_string(&m).unwrap_or_default(),
AgentValue::Json(j) => j.to_string(),
AgentValue::Null => String::new(),
}
}
pub fn as_texts(&self) -> Option<&Vec<String>> {
match self {
AgentValue::Texts(v) => Some(v),
_ => None,
}
}
pub fn as_map(&self) -> Option<&HashMap<String, String>> {
match self {
AgentValue::Map(m) => Some(m),
_ => None,
}
}
}
impl From<String> for AgentValue {
fn from(s: String) -> Self {
AgentValue::Text(s)
}
}
impl From<&str> for AgentValue {
fn from(s: &str) -> Self {
AgentValue::Text(s.to_string())
}
}
impl From<Vec<String>> for AgentValue {
fn from(v: Vec<String>) -> Self {
AgentValue::Texts(v)
}
}
impl From<HashMap<String, String>> for AgentValue {
fn from(m: HashMap<String, String>) -> Self {
AgentValue::Map(m)
}
}
pub type RouterFn =
Arc<dyn Fn(AgentValue) -> Pin<Box<dyn Future<Output = String> + Send>> + Send + Sync>;
pub type TransformFn =
Arc<dyn Fn(AgentValue) -> Pin<Box<dyn Future<Output = AgentValue> + Send>> + Send + Sync>;
pub type JoinFn = Arc<
dyn Fn(HashMap<String, AgentValue>) -> Pin<Box<dyn Future<Output = AgentValue> + Send>>
+ Send
+ Sync,
>;
pub struct AgentNode {
pub id: String,
pub name: String,
pub node_type: AgentNodeType,
agent: Option<Arc<LLMAgent>>,
router: Option<RouterFn>,
transform: Option<TransformFn>,
join_fn: Option<JoinFn>,
wait_for: Vec<String>,
prompt_template: Option<String>,
session_id: Option<String>,
}
impl AgentNode {
pub fn start() -> Self {
Self {
id: "start".to_string(),
name: "Start".to_string(),
node_type: AgentNodeType::Start,
agent: None,
router: None,
transform: None,
join_fn: None,
wait_for: Vec::new(),
prompt_template: None,
session_id: None,
}
}
pub fn end() -> Self {
Self {
id: "end".to_string(),
name: "End".to_string(),
node_type: AgentNodeType::End,
agent: None,
router: None,
transform: None,
join_fn: None,
wait_for: Vec::new(),
prompt_template: None,
session_id: None,
}
}
pub fn agent(id: impl Into<String>, agent: Arc<LLMAgent>) -> Self {
let id = id.into();
Self {
name: id.clone(),
id,
node_type: AgentNodeType::Agent,
agent: Some(agent),
router: None,
transform: None,
join_fn: None,
wait_for: Vec::new(),
prompt_template: None,
session_id: None,
}
}
pub fn router<F, Fut>(id: impl Into<String>, router_fn: F) -> Self
where
F: Fn(AgentValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = String> + Send + 'static,
{
let id = id.into();
Self {
name: id.clone(),
id,
node_type: AgentNodeType::Router,
agent: None,
router: Some(Arc::new(move |input| Box::pin(router_fn(input)))),
transform: None,
join_fn: None,
wait_for: Vec::new(),
prompt_template: None,
session_id: None,
}
}
pub fn parallel(id: impl Into<String>) -> Self {
let id = id.into();
Self {
name: id.clone(),
id,
node_type: AgentNodeType::Parallel,
agent: None,
router: None,
transform: None,
join_fn: None,
wait_for: Vec::new(),
prompt_template: None,
session_id: None,
}
}
pub fn join(id: impl Into<String>, wait_for: Vec<String>) -> Self {
let id = id.into();
Self {
name: id.clone(),
id,
node_type: AgentNodeType::Join,
agent: None,
router: None,
transform: None,
join_fn: None,
wait_for,
prompt_template: None,
session_id: None,
}
}
pub fn join_with<F, Fut>(id: impl Into<String>, wait_for: Vec<String>, join_fn: F) -> Self
where
F: Fn(HashMap<String, AgentValue>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = AgentValue> + Send + 'static,
{
let id = id.into();
Self {
name: id.clone(),
id,
node_type: AgentNodeType::Join,
agent: None,
router: None,
transform: None,
join_fn: Some(Arc::new(move |inputs| Box::pin(join_fn(inputs)))),
wait_for,
prompt_template: None,
session_id: None,
}
}
pub fn transform<F, Fut>(id: impl Into<String>, transform_fn: F) -> Self
where
F: Fn(AgentValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = AgentValue> + Send + 'static,
{
let id = id.into();
Self {
name: id.clone(),
id,
node_type: AgentNodeType::Transform,
agent: None,
router: None,
transform: Some(Arc::new(move |input| Box::pin(transform_fn(input)))),
join_fn: None,
wait_for: Vec::new(),
prompt_template: None,
session_id: None,
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn with_prompt_template(mut self, template: impl Into<String>) -> Self {
self.prompt_template = Some(template.into());
self
}
pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
}
#[derive(Debug, Clone)]
pub struct AgentEdge {
pub from: String,
pub to: String,
pub condition: Option<String>,
}
impl AgentEdge {
pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
Self {
from: from.into(),
to: to.into(),
condition: None,
}
}
pub fn conditional(
from: impl Into<String>,
to: impl Into<String>,
condition: impl Into<String>,
) -> Self {
Self {
from: from.into(),
to: to.into(),
condition: Some(condition.into()),
}
}
}
pub struct AgentWorkflowContext {
pub workflow_id: String,
pub execution_id: String,
node_outputs: Arc<RwLock<HashMap<String, AgentValue>>>,
shared_session_id: Option<String>,
variables: Arc<RwLock<HashMap<String, String>>>,
}
impl AgentWorkflowContext {
pub fn new(workflow_id: impl Into<String>) -> Self {
Self {
workflow_id: workflow_id.into(),
execution_id: uuid::Uuid::now_v7().to_string(),
node_outputs: Arc::new(RwLock::new(HashMap::new())),
shared_session_id: None,
variables: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn with_shared_session(mut self, session_id: impl Into<String>) -> Self {
self.shared_session_id = Some(session_id.into());
self
}
pub async fn set_output(&self, node_id: &str, value: AgentValue) {
let mut outputs = self.node_outputs.write().await;
outputs.insert(node_id.to_string(), value);
}
pub async fn get_output(&self, node_id: &str) -> Option<AgentValue> {
let outputs = self.node_outputs.read().await;
outputs.get(node_id).cloned()
}
pub async fn get_outputs(&self, node_ids: &[String]) -> HashMap<String, AgentValue> {
let outputs = self.node_outputs.read().await;
node_ids
.iter()
.filter_map(|id| outputs.get(id).map(|v| (id.clone(), v.clone())))
.collect()
}
pub async fn set_variable(&self, key: &str, value: &str) {
let mut vars = self.variables.write().await;
vars.insert(key.to_string(), value.to_string());
}
pub async fn get_variable(&self, key: &str) -> Option<String> {
let vars = self.variables.read().await;
vars.get(key).cloned()
}
}
impl Clone for AgentWorkflowContext {
fn clone(&self) -> Self {
Self {
workflow_id: self.workflow_id.clone(),
execution_id: self.execution_id.clone(),
node_outputs: self.node_outputs.clone(),
shared_session_id: self.shared_session_id.clone(),
variables: self.variables.clone(),
}
}
}
pub struct AgentWorkflow {
pub id: String,
pub name: String,
nodes: HashMap<String, AgentNode>,
edges: Vec<AgentEdge>,
adjacency: HashMap<String, Vec<AgentEdge>>,
}
impl AgentWorkflow {
pub fn new(id: impl Into<String>) -> AgentWorkflowBuilder {
AgentWorkflowBuilder::new(id)
}
pub async fn run(&self, input: impl Into<AgentValue>) -> LLMResult<AgentValue> {
let ctx = AgentWorkflowContext::new(&self.id);
self.run_with_context(&ctx, input).await
}
pub async fn run_with_context(
&self,
ctx: &AgentWorkflowContext,
input: impl Into<AgentValue>,
) -> LLMResult<AgentValue> {
let input = input.into();
let mut current_node_id = "start".to_string();
let mut current_input = input;
loop {
let node = self
.nodes
.get(¤t_node_id)
.ok_or_else(|| LLMError::Other(format!("Node '{}' not found", current_node_id)))?;
let output = self.execute_node(ctx, node, current_input.clone()).await?;
ctx.set_output(¤t_node_id, output.clone()).await;
match self.get_next_node(¤t_node_id, &output).await {
Some(next_id) => {
current_node_id = next_id;
current_input = output;
}
None => {
return Ok(output);
}
}
}
}
async fn execute_node(
&self,
ctx: &AgentWorkflowContext,
node: &AgentNode,
input: AgentValue,
) -> LLMResult<AgentValue> {
match node.node_type {
AgentNodeType::Start | AgentNodeType::End => Ok(input),
AgentNodeType::Agent => {
let agent = node
.agent
.as_ref()
.ok_or_else(|| LLMError::Other("Agent not set".to_string()))?;
let prompt = if let Some(ref template) = node.prompt_template {
template.replace("{input}", &input.clone().into_text())
} else {
input.clone().into_text()
};
let session_id = node
.session_id
.clone()
.or_else(|| ctx.shared_session_id.clone());
let response = if let Some(sid) = session_id {
let _ = agent.get_or_create_session(&sid).await;
agent.chat_with_session(&sid, &prompt).await?
} else {
agent.ask(&prompt).await?
};
Ok(AgentValue::Text(response))
}
AgentNodeType::Router => {
let router = node
.router
.as_ref()
.ok_or_else(|| LLMError::Other("Router function not set".to_string()))?;
let _route = router(input.clone()).await;
Ok(input)
}
AgentNodeType::Parallel => {
Ok(input)
}
AgentNodeType::Join => {
let outputs = ctx.get_outputs(&node.wait_for).await;
if let Some(ref join_fn) = node.join_fn {
Ok(join_fn(outputs).await)
} else {
let texts: Vec<String> = outputs.into_values().map(|v| v.into_text()).collect();
Ok(AgentValue::Texts(texts))
}
}
AgentNodeType::Transform => {
let transform = node
.transform
.as_ref()
.ok_or_else(|| LLMError::Other("Transform function not set".to_string()))?;
Ok(transform(input).await)
}
}
}
async fn get_next_node(&self, current_id: &str, output: &AgentValue) -> Option<String> {
let node = self.nodes.get(current_id)?;
if matches!(node.node_type, AgentNodeType::End) {
return None;
}
let edges = self.adjacency.get(current_id)?;
if matches!(node.node_type, AgentNodeType::Router) {
if let Some(ref router) = node.router {
let route = router(output.clone()).await;
for edge in edges {
if edge.condition.as_ref() == Some(&route) {
return Some(edge.to.clone());
}
}
}
for edge in edges {
if edge.condition.is_none() {
return Some(edge.to.clone());
}
}
return None;
}
edges.first().map(|e| e.to.clone())
}
pub fn get_node(&self, id: &str) -> Option<&AgentNode> {
self.nodes.get(id)
}
pub fn node_ids(&self) -> Vec<&str> {
self.nodes.keys().map(|s| s.as_str()).collect()
}
}
pub struct AgentWorkflowBuilder {
id: String,
name: String,
nodes: HashMap<String, AgentNode>,
edges: Vec<AgentEdge>,
current_node: Option<String>,
}
impl AgentWorkflowBuilder {
pub fn new(id: impl Into<String>) -> Self {
let id = id.into();
let mut builder = Self {
name: id.clone(),
id,
nodes: HashMap::new(),
edges: Vec::new(),
current_node: None,
};
builder
.nodes
.insert("start".to_string(), AgentNode::start());
builder.current_node = Some("start".to_string());
builder
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn add_agent(mut self, id: impl Into<String>, agent: Arc<LLMAgent>) -> Self {
let id = id.into();
let node = AgentNode::agent(&id, agent);
self.nodes.insert(id, node);
self
}
pub fn add_agent_with_template(
mut self,
id: impl Into<String>,
agent: Arc<LLMAgent>,
template: impl Into<String>,
) -> Self {
let id = id.into();
let node = AgentNode::agent(&id, agent).with_prompt_template(template);
self.nodes.insert(id, node);
self
}
pub fn add_router<F, Fut>(mut self, id: impl Into<String>, router_fn: F) -> Self
where
F: Fn(AgentValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = String> + Send + 'static,
{
let id = id.into();
let node = AgentNode::router(&id, router_fn);
self.nodes.insert(id, node);
self
}
pub fn add_llm_router(
mut self,
id: impl Into<String>,
router_agent: Arc<LLMAgent>,
routes: Vec<String>,
) -> Self {
let id = id.into();
let routes_str = routes.join(", ");
let prompt = format!(
"Based on the following input, choose the most appropriate route. \
Available routes: {}. \
Respond with ONLY the route name, nothing else.\n\nInput: {{input}}",
routes_str
);
let routes_clone = routes.clone();
let router_fn = move |input: AgentValue| {
let agent = router_agent.clone();
let prompt = prompt.replace("{input}", &input.into_text());
let valid_routes = routes_clone.clone();
async move {
match agent.ask(&prompt).await {
Ok(response) => {
let route = response.trim().to_string();
if valid_routes.contains(&route) {
route
} else {
valid_routes.first().cloned().unwrap_or_default()
}
}
Err(_) => valid_routes.first().cloned().unwrap_or_default(),
}
}
};
let node = AgentNode::router(&id, router_fn);
self.nodes.insert(id, node);
self
}
pub fn add_transform<F, Fut>(mut self, id: impl Into<String>, transform_fn: F) -> Self
where
F: Fn(AgentValue) -> Fut + Send + Sync + 'static,
Fut: Future<Output = AgentValue> + Send + 'static,
{
let id = id.into();
let node = AgentNode::transform(&id, transform_fn);
self.nodes.insert(id, node);
self
}
pub fn add_parallel(mut self, id: impl Into<String>) -> Self {
let id = id.into();
let node = AgentNode::parallel(&id);
self.nodes.insert(id, node);
self
}
pub fn add_join(mut self, id: impl Into<String>, wait_for: Vec<&str>) -> Self {
let id = id.into();
let wait_for: Vec<String> = wait_for.into_iter().map(|s| s.to_string()).collect();
let node = AgentNode::join(&id, wait_for);
self.nodes.insert(id, node);
self
}
pub fn add_join_with<F, Fut>(
mut self,
id: impl Into<String>,
wait_for: Vec<&str>,
join_fn: F,
) -> Self
where
F: Fn(HashMap<String, AgentValue>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = AgentValue> + Send + 'static,
{
let id = id.into();
let wait_for: Vec<String> = wait_for.into_iter().map(|s| s.to_string()).collect();
let node = AgentNode::join_with(&id, wait_for, join_fn);
self.nodes.insert(id, node);
self
}
pub fn connect(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
self.edges.push(AgentEdge::new(from, to));
self
}
pub fn connect_on(
mut self,
from: impl Into<String>,
to: impl Into<String>,
condition: impl Into<String>,
) -> Self {
self.edges.push(AgentEdge::conditional(from, to, condition));
self
}
pub fn chain<S: Into<String> + Clone>(mut self, node_ids: impl IntoIterator<Item = S>) -> Self {
let ids: Vec<String> = node_ids.into_iter().map(|s| s.into()).collect();
if ids.is_empty() {
return self;
}
self.edges.push(AgentEdge::new("start", &ids[0]));
for i in 0..ids.len() - 1 {
self.edges.push(AgentEdge::new(&ids[i], &ids[i + 1]));
}
self.nodes.insert("end".to_string(), AgentNode::end());
self.edges.push(AgentEdge::new(ids.last().unwrap(), "end"));
self
}
pub fn parallel_agents(
mut self,
parallel_id: impl Into<String>,
agent_ids: Vec<&str>,
join_id: impl Into<String>,
) -> Self {
let parallel_id = parallel_id.into();
let join_id = join_id.into();
self.nodes
.insert(parallel_id.clone(), AgentNode::parallel(¶llel_id));
let wait_for: Vec<String> = agent_ids.iter().map(|s| s.to_string()).collect();
self.nodes
.insert(join_id.clone(), AgentNode::join(&join_id, wait_for));
for agent_id in &agent_ids {
self.edges.push(AgentEdge::new(¶llel_id, *agent_id));
self.edges.push(AgentEdge::new(*agent_id, &join_id));
}
self
}
pub fn build(self) -> AgentWorkflow {
let mut adjacency: HashMap<String, Vec<AgentEdge>> = HashMap::new();
for edge in &self.edges {
adjacency
.entry(edge.from.clone())
.or_default()
.push(edge.clone());
}
AgentWorkflow {
id: self.id,
name: self.name,
nodes: self.nodes,
edges: self.edges,
adjacency,
}
}
}
pub fn agent_chain<S: Into<String>>(
id: S,
agents: Vec<(impl Into<String>, Arc<LLMAgent>)>,
) -> AgentWorkflow {
let mut builder = AgentWorkflowBuilder::new(id);
let mut ids = Vec::new();
for (agent_id, agent) in agents {
let agent_id = agent_id.into();
ids.push(agent_id.clone());
builder = builder.add_agent(agent_id, agent);
}
builder.chain(ids).build()
}
pub fn agent_parallel<S: Into<String>>(
id: S,
agents: Vec<(impl Into<String>, Arc<LLMAgent>)>,
) -> AgentWorkflow {
let mut builder = AgentWorkflowBuilder::new(id);
let mut ids = Vec::new();
for (agent_id, agent) in agents {
let agent_id = agent_id.into();
ids.push(agent_id.clone());
builder = builder.add_agent(agent_id, agent);
}
let ids_ref: Vec<&str> = ids.iter().map(|s| s.as_str()).collect();
builder
.parallel_agents("parallel", ids_ref, "join")
.connect("start", "parallel")
.connect("join", "end")
.build()
}
pub fn agent_router<S: Into<String>>(
id: S,
router_agent: Arc<LLMAgent>,
routes: Vec<(impl Into<String>, Arc<LLMAgent>)>,
) -> AgentWorkflow {
let mut builder = AgentWorkflowBuilder::new(id);
let mut route_names = Vec::new();
for (route_id, agent) in routes {
let route_id = route_id.into();
route_names.push(route_id.clone());
builder = builder.add_agent(&route_id, agent);
}
builder = builder.add_llm_router("router", router_agent, route_names.clone());
builder = builder.connect("start", "router");
builder.nodes.insert("end".to_string(), AgentNode::end());
for route_name in &route_names {
builder = builder.connect_on("router", route_name, route_name);
builder = builder.connect(route_name, "end");
}
builder.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_value_conversions() {
let v: AgentValue = "hello".into();
assert_eq!(v.as_text(), Some("hello"));
let v: AgentValue = "world".to_string().into();
assert_eq!(v.as_text(), Some("world"));
let v: AgentValue = vec!["a".to_string(), "b".to_string()].into();
assert_eq!(v.as_texts().map(|v| v.len()), Some(2));
}
#[test]
fn test_workflow_builder() {
let workflow = AgentWorkflowBuilder::new("test")
.with_name("Test Workflow")
.add_transform("uppercase", |input: AgentValue| async move {
AgentValue::Text(input.into_text().to_uppercase())
})
.chain(["uppercase"])
.build();
assert_eq!(workflow.node_ids().len(), 3); }
#[test]
fn test_chain_builder() {
let workflow = AgentWorkflowBuilder::new("chain-test")
.add_transform("step1", |input| async move { input })
.add_transform("step2", |input| async move { input })
.add_transform("step3", |input| async move { input })
.chain(["step1", "step2", "step3"])
.build();
assert!(workflow.get_node("start").is_some());
assert!(workflow.get_node("end").is_some());
assert!(workflow.get_node("step1").is_some());
assert!(workflow.get_node("step2").is_some());
assert!(workflow.get_node("step3").is_some());
}
}