1use 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
18pub 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 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 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 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 pub fn len(&self) -> usize {
93 self.branches.len()
94 }
95
96 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 async fn invoke(
111 &self,
112 input: I,
113 config: Option<RunnableConfig>,
114 ) -> Result<O, LcelError> {
115 for (condition, branch) in &self.branches {
116 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 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 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 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}