use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::Value;
use crate::error::{GraphError, Result};
use crate::graph::CompiledGraph;
use crate::interrupt::Interrupt;
use crate::node::{ExecutionConfig, Node, NodeContext, NodeOutput};
use crate::state::{State, StateSchema};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChannelSide {
Parent,
Child,
}
impl std::fmt::Display for ChannelSide {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parent => write!(f, "parent"),
Self::Child => write!(f, "subgraph"),
}
}
}
pub struct SubgraphNode {
name: String,
graph: Arc<CompiledGraph>,
inputs: Vec<(String, String)>,
outputs: Vec<(String, String)>,
share_by_name: bool,
}
impl SubgraphNode {
pub fn new(name: impl Into<String>, graph: Arc<CompiledGraph>) -> Self {
Self {
name: name.into(),
graph,
inputs: Vec::new(),
outputs: Vec::new(),
share_by_name: true,
}
}
pub fn with_input(mut self, parent: impl Into<String>, child: impl Into<String>) -> Self {
self.inputs.push((parent.into(), child.into()));
self
}
pub fn with_output(mut self, child: impl Into<String>, parent: impl Into<String>) -> Self {
self.outputs.push((child.into(), parent.into()));
self
}
pub fn isolated(mut self) -> Self {
self.share_by_name = false;
self
}
pub fn graph(&self) -> &Arc<CompiledGraph> {
&self.graph
}
fn shared_with(&self, parent: &StateSchema) -> Vec<String> {
if !self.share_by_name {
return Vec::new();
}
let mut names: Vec<String> = self
.graph
.schema
.channels
.keys()
.filter(|name| parent.channels.contains_key(*name))
.cloned()
.collect();
names.sort();
names
}
fn project_in(&self, parent_state: &State, parent_schema: &StateSchema) -> State {
let mut input = State::new();
for name in self.shared_with(parent_schema) {
if let Some(value) = parent_state.get(&name) {
input.insert(name, value.clone());
}
}
for (parent_name, child_name) in &self.inputs {
if let Some(value) = parent_state.get(parent_name) {
input.insert(child_name.clone(), value.clone());
}
}
input
}
fn project_out(
&self,
child_state: &State,
parent_schema: &StateSchema,
) -> HashMap<String, Value> {
let mut updates = HashMap::new();
for name in self.shared_with(parent_schema) {
if let Some(value) = child_state.get(&name) {
updates.insert(name, value.clone());
}
}
for (child_name, parent_name) in &self.outputs {
if let Some(value) = child_state.get(child_name) {
updates.insert(parent_name.clone(), value.clone());
}
}
updates
}
fn child_thread(&self, parent_thread: &str) -> String {
format!("{parent_thread}/{}", self.name)
}
}
#[async_trait]
impl Node for SubgraphNode {
fn name(&self) -> &str {
&self.name
}
fn validate_against(&self, parent: &StateSchema) -> Result<()> {
let child = &self.graph.schema;
let mismatch = |channel: &str, side: ChannelSide| {
Err(GraphError::SubgraphChannelMismatch {
subgraph: self.name.clone(),
channel: channel.to_string(),
side: side.to_string(),
})
};
for (parent_name, child_name) in &self.inputs {
if !parent.channels.contains_key(parent_name) {
return mismatch(parent_name, ChannelSide::Parent);
}
if !child.channels.contains_key(child_name) {
return mismatch(child_name, ChannelSide::Child);
}
}
for (child_name, parent_name) in &self.outputs {
if !child.channels.contains_key(child_name) {
return mismatch(child_name, ChannelSide::Child);
}
if !parent.channels.contains_key(parent_name) {
return mismatch(parent_name, ChannelSide::Parent);
}
}
if self.graph.can_pause() && !self.graph.has_checkpointer() {
return Err(GraphError::InvalidGraph(format!(
"subgraph '{}' has interrupt gates but no checkpointer, so a pause \
inside it could not be resumed and its finished work would run \
again. Add one with with_checkpointer",
self.name
)));
}
if self.inputs.is_empty() && self.outputs.is_empty() && self.shared_with(parent).is_empty()
{
return Err(GraphError::InvalidGraph(format!(
"subgraph '{}' exchanges no channels with its parent. Name them with \
with_input and with_output, or share a channel name",
self.name
)));
}
Ok(())
}
async fn execute(&self, ctx: &NodeContext) -> Result<NodeOutput> {
let parent_schema = ctx.parent_schema().ok_or_else(|| {
GraphError::InvalidGraph(format!(
"subgraph '{}' ran without its parent's schema. This is an executor \
defect, not a configuration error",
self.name
))
})?;
let input = self.project_in(&ctx.state, &parent_schema);
let thread = self.child_thread(&ctx.config.thread_id);
let config = ExecutionConfig::new(&thread);
match self.graph.invoke_detailed(input, config).await {
Ok(outcome) => {
let mut output = NodeOutput::new()
.with_updates(self.project_out(&outcome.state, &parent_schema));
if let Some(targets) = outcome.goto_parent {
output = output.with_goto(targets);
}
Ok(output)
}
Err(GraphError::Interrupted(inner)) => {
let message = match &inner.interrupt {
Interrupt::Dynamic { message, .. } => message.clone(),
other => other.to_string(),
};
let data = match &inner.interrupt {
Interrupt::Dynamic { data, .. } => data.clone(),
_ => None,
};
let mut payload = serde_json::Map::new();
payload.insert("subgraph".to_string(), Value::String(self.name.clone()));
payload.insert("thread".to_string(), Value::String(thread));
if let Some(data) = data {
payload.insert("data".to_string(), data);
}
Ok(NodeOutput::interrupt_with_data(
&format!("{}: {message}", self.name),
Value::Object(payload),
))
}
Err(error) => Err(error),
}
}
}