Skip to main content

lc_core/runnables/
branch.rs

1// lc-core/src/runnables/branch.rs
2//! RunnableBranch - conditional routing in LCEL pipelines.
3//!
4//! `RunnableBranch` evaluates conditions sequentially and executes
5//! the first matching branch. If no condition matches, the default
6//! branch is executed.
7
8use super::any::{into_runnable_any, RunnableAny};
9use super::config::RunnableConfig;
10use super::error::LcelError;
11use super::lambda::RunnableLambda;
12use super::runnable_trait::Runnable;
13use async_trait::async_trait;
14use futures_util::Stream;
15use std::any::Any;
16use std::pin::Pin;
17
18/// A `Runnable` that routes input to different branches based on conditions.
19///
20/// Conditions are evaluated in order; the first matching branch wins.
21/// If no condition matches, the default branch is used.
22///
23/// The input type `I` must be `Clone` because the input may need to be
24/// evaluated against multiple conditions before a match is found.
25///
26/// # Example
27///
28/// ```rust,ignore
29/// let branch = RunnableBranch::new(default_handler)
30///     .when_fn(|input: &String| input.len() > 100, long_handler)
31///     .when_fn(|input: &String| input.starts_with('?'), question_handler);
32/// ```
33pub struct RunnableBranch<I: Send + Sync + 'static, O: Send + Sync + 'static> {
34    branches: Vec<(Box<dyn RunnableAny>, Box<dyn RunnableAny>)>,
35    default: Box<dyn RunnableAny>,
36    _marker: std::marker::PhantomData<(I, O)>,
37}
38
39impl<I: Send + Sync + 'static, O: Send + Sync + 'static> std::fmt::Debug for RunnableBranch<I, O> {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        f.debug_struct("RunnableBranch")
42            .field("branches", &self.branches.len())
43            .field("input", &std::any::type_name::<I>())
44            .field("output", &std::any::type_name::<O>())
45            .finish()
46    }
47}
48
49impl<I: Clone + Send + Sync + 'static, O: Send + Sync + 'static> RunnableBranch<I, O> {
50    /// Create a new branch with a default runnable.
51    pub fn new<R>(default: R) -> Self
52    where
53        R: Runnable<I, O> + 'static,
54        R::Error: Into<LcelError>,
55    {
56        Self {
57            branches: Vec::new(),
58            default: into_runnable_any(default),
59            _marker: std::marker::PhantomData,
60        }
61    }
62
63    /// Add a branch with a condition runnable and a branch runnable.
64    ///
65    /// The condition runnable takes `I` and returns `bool`.
66    /// If the condition returns `true`, the branch runnable is executed.
67    pub fn when<R1, R2>(mut self, condition: R1, branch: R2) -> Self
68    where
69        R1: Runnable<I, bool> + 'static,
70        R1::Error: Into<LcelError>,
71        R2: Runnable<I, O> + 'static,
72        R2::Error: Into<LcelError>,
73    {
74        self.branches.push((into_runnable_any(condition), into_runnable_any(branch)));
75        self
76    }
77
78    /// Add a branch with a synchronous condition closure.
79    ///
80    /// Convenience method that wraps the closure in a `RunnableLambda`.
81    pub fn when_fn<F, R2>(self, condition: F, branch: R2) -> Self
82    where
83        F: Fn(&I) -> bool + Send + Sync + 'static,
84        R2: Runnable<I, O> + 'static,
85        R2::Error: Into<LcelError>,
86    {
87        let condition_lambda = RunnableLambda::new_sync(move |input: I| condition(&input));
88        self.when(condition_lambda, branch)
89    }
90
91    /// Number of branches (excluding default).
92    pub fn len(&self) -> usize {
93        self.branches.len()
94    }
95
96    /// Whether there are no branches (only default).
97    pub fn is_empty(&self) -> bool {
98        self.branches.is_empty()
99    }
100}
101
102#[async_trait]
103impl<I: Clone + Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O>
104    for RunnableBranch<I, O>
105{
106    type Error = LcelError;
107
108    /// Evaluate conditions in order, execute the first matching branch.
109    /// If no condition matches, execute the default.
110    async fn invoke(
111        &self,
112        input: I,
113        config: Option<RunnableConfig>,
114    ) -> Result<O, LcelError> {
115        for (condition, branch) in &self.branches {
116            // Clone input for condition evaluation (input is preserved for branch)
117            let cond_input: Box<dyn Any + Send> = Box::new(input.clone());
118            let cond_result = condition.invoke_any(cond_input, config.clone()).await?;
119            let matches: bool = cond_result
120                .downcast::<bool>()
121                .map(|b| *b)
122                .map_err(|_| {
123                    LcelError::TypeMismatch("branch condition must return bool".to_string())
124                })?;
125
126            if matches {
127                let branch_input: Box<dyn Any + Send> = Box::new(input);
128                let result = branch.invoke_any(branch_input, config).await?;
129                return result.downcast::<O>().map(|b| *b).map_err(|_| {
130                    LcelError::TypeMismatch(format!(
131                        "branch output downcast: expected {}",
132                        std::any::type_name::<O>()
133                    ))
134                });
135            }
136        }
137
138        // No condition matched, use default
139        let default_input: Box<dyn Any + Send> = Box::new(input);
140        let result = self.default.invoke_any(default_input, config).await?;
141        result
142            .downcast::<O>()
143            .map(|b| *b)
144            .map_err(|_| LcelError::TypeMismatch(format!(
145                "branch default output downcast: expected {}",
146                std::any::type_name::<O>()
147            )))
148    }
149
150    /// Stream: invoke and return single-element stream.
151    async fn stream(
152        &self,
153        input: I,
154        config: Option<RunnableConfig>,
155    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
156        let result = self.invoke(input, config).await?;
157        Ok(Box::pin(futures_util::stream::once(async move { Ok(result) })))
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::RunnableConfig;
165    use async_trait::async_trait;
166    use futures_util::StreamExt;
167
168    struct EchoDefault;
169
170    #[async_trait]
171    impl Runnable<String, String> for EchoDefault {
172        type Error = std::convert::Infallible;
173
174        async fn invoke(
175            &self,
176            input: String,
177            _config: Option<RunnableConfig>,
178        ) -> Result<String, Self::Error> {
179            Ok(format!("default: {}", input))
180        }
181    }
182
183    struct LongHandler;
184
185    #[async_trait]
186    impl Runnable<String, String> for LongHandler {
187        type Error = std::convert::Infallible;
188
189        async fn invoke(
190            &self,
191            input: String,
192            _config: Option<RunnableConfig>,
193        ) -> Result<String, Self::Error> {
194            Ok(format!("long: {}", input))
195        }
196    }
197
198    #[tokio::test]
199    async fn branch_matches_condition() {
200        let branch = RunnableBranch::new(EchoDefault).when_fn(
201            |input: &String| input.len() > 5,
202            LongHandler,
203        );
204
205        let result = branch.invoke("hello world".to_string(), None).await.unwrap();
206        assert_eq!(result, "long: hello world");
207    }
208
209    #[tokio::test]
210    async fn branch_falls_to_default() {
211        let branch = RunnableBranch::new(EchoDefault).when_fn(
212            |input: &String| input.len() > 100,
213            LongHandler,
214        );
215
216        let result = branch.invoke("hi".to_string(), None).await.unwrap();
217        assert_eq!(result, "default: hi");
218    }
219
220    #[tokio::test]
221    async fn branch_first_match_wins() {
222        let branch = RunnableBranch::new(EchoDefault)
223            .when_fn(
224                |input: &String| input.starts_with('h'),
225                RunnableLambda::new_sync(|s: String| format!("starts-h: {}", s)),
226            )
227            .when_fn(
228                |input: &String| input.len() > 3,
229                RunnableLambda::new_sync(|s: String| format!("long: {}", s)),
230            );
231
232        // "hello" starts with 'h', so first branch wins
233        let result = branch.invoke("hello".to_string(), None).await.unwrap();
234        assert_eq!(result, "starts-h: hello");
235    }
236
237    #[tokio::test]
238    async fn branch_stream_works() {
239        let branch = RunnableBranch::new(EchoDefault).when_fn(
240            |input: &String| input.len() > 5,
241            LongHandler,
242        );
243
244        let mut stream = branch.stream("hello world".to_string(), None).await.unwrap();
245        let result = stream.next().await.unwrap().unwrap();
246        assert_eq!(result, "long: hello world");
247    }
248}