1use super::any::{into_runnable_any, RunnableAny};
18use super::config::RunnableConfig;
19use super::error::LcelError;
20use super::runnable_trait::Runnable;
21use async_trait::async_trait;
22use futures_util::{Stream, StreamExt};
23use std::any::Any;
24use std::marker::PhantomData;
25use std::pin::Pin;
26
27pub struct RunnableWithFallbacks<I: Send + Sync + 'static, O: Send + Sync + 'static> {
37 primary: Box<dyn RunnableAny>,
38 fallbacks: Vec<Box<dyn RunnableAny>>,
39 _marker: PhantomData<(I, O)>,
40}
41
42impl<I: Send + Sync + 'static, O: Send + Sync + 'static> std::fmt::Debug
43 for RunnableWithFallbacks<I, O>
44{
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("RunnableWithFallbacks")
47 .field("fallbacks", &self.fallbacks.len())
48 .field("input", &std::any::type_name::<I>())
49 .field("output", &std::any::type_name::<O>())
50 .finish()
51 }
52}
53
54impl<I: Clone + Send + Sync + 'static, O: Send + Sync + 'static> RunnableWithFallbacks<I, O> {
55 pub fn new<R>(primary: R, fallbacks: Vec<Box<dyn RunnableAny>>) -> Self
57 where
58 R: Runnable<I, O> + 'static,
59 R::Error: Into<LcelError>,
60 {
61 Self {
62 primary: into_runnable_any(primary),
63 fallbacks,
64 _marker: PhantomData,
65 }
66 }
67
68 pub fn fallback_count(&self) -> usize {
70 self.fallbacks.len()
71 }
72
73 async fn try_all(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
76 let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
78 match self.primary.invoke_any(boxed_input, config.clone()).await {
79 Ok(result) => result.downcast::<O>().map(|b| *b).map_err(|_| {
80 LcelError::TypeMismatch(format!(
81 "fallback primary output downcast: expected {}",
82 std::any::type_name::<O>()
83 ))
84 }),
85 Err(primary_error) => {
86 for fallback in &self.fallbacks {
88 let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
89 match fallback.invoke_any(boxed_input, config.clone()).await {
90 Ok(result) => {
91 return result.downcast::<O>().map(|b| *b).map_err(|_| {
92 LcelError::TypeMismatch(format!(
93 "fallback output downcast: expected {}",
94 std::any::type_name::<O>()
95 ))
96 });
97 }
98 Err(_) => continue,
99 }
100 }
101 Err(primary_error)
103 }
104 }
105 }
106}
107
108#[async_trait]
109impl<I: Clone + Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O>
110 for RunnableWithFallbacks<I, O>
111{
112 type Error = LcelError;
113
114 async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
116 self.try_all(input, config).await
117 }
118
119 async fn stream(
122 &self,
123 input: I,
124 config: Option<RunnableConfig>,
125 ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
126 let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
128 match self.primary.stream_any(boxed_input, config.clone()).await {
129 Ok(stream) => {
130 let output_stream = stream.map(|result| {
131 result.and_then(|boxed| {
132 boxed.downcast::<O>().map(|b| *b).map_err(|_| {
133 LcelError::TypeMismatch(format!(
134 "fallback stream downcast: expected {}",
135 std::any::type_name::<O>()
136 ))
137 })
138 })
139 });
140 return Ok(Box::pin(output_stream));
141 }
142 Err(primary_error) => {
143 for fallback in &self.fallbacks {
145 let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
146 match fallback.stream_any(boxed_input, config.clone()).await {
147 Ok(stream) => {
148 let output_stream = stream.map(|result| {
149 result.and_then(|boxed| {
150 boxed.downcast::<O>().map(|b| *b).map_err(|_| {
151 LcelError::TypeMismatch(format!(
152 "fallback stream downcast: expected {}",
153 std::any::type_name::<O>()
154 ))
155 })
156 })
157 });
158 return Ok(Box::pin(output_stream));
159 }
160 Err(_) => continue,
161 }
162 }
163 Err(primary_error)
164 }
165 }
166 }
167
168 async fn batch(
170 &self,
171 inputs: Vec<I>,
172 config: Option<RunnableConfig>,
173 ) -> Result<Vec<O>, LcelError> {
174 let mut results = Vec::with_capacity(inputs.len());
175 for input in inputs {
176 results.push(self.invoke(input, config.clone()).await?);
177 }
178 Ok(results)
179 }
180
181 async fn transform(
183 &self,
184 input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
185 config: Option<RunnableConfig>,
186 ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
187 let mut items = Vec::new();
189 let mut input = input;
190 while let Some(item) = input.next().await {
191 items.push(item?);
192 }
193
194 if let Some(last) = items.into_iter().last() {
195 let result = self.invoke(last, config).await?;
196 Ok(Box::pin(futures_util::stream::once(
197 async move { Ok(result) },
198 )))
199 } else {
200 Ok(Box::pin(futures_util::stream::empty()))
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::runnables::{RunnableExt, RunnableLambda};
209 use futures_util::StreamExt;
210
211 #[tokio::test]
212 async fn primary_succeeds_no_fallback() {
213 let primary = RunnableLambda::new_sync(|x: i32| x * 2);
214 let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
215
216 let with_fallbacks = primary.with_fallbacks(vec![fallback]);
217 let result = with_fallbacks.invoke(5, None).await.unwrap();
218 assert_eq!(result, 10); }
220
221 #[tokio::test]
222 async fn primary_fails_fallback_succeeds() {
223 let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
224 Err(LcelError::Other("primary failed".to_string()))
225 });
226 let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
227
228 let with_fallbacks = primary.with_fallbacks(vec![fallback]);
229 let result = with_fallbacks.invoke(5, None).await.unwrap();
230 assert_eq!(result, 15); }
232
233 #[tokio::test]
234 async fn all_fail_returns_primary_error() {
235 let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
236 Err(LcelError::Provider("openai timeout".to_string()))
237 });
238 let fallback = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
239 Err(LcelError::Provider("anthropic timeout".to_string()))
240 });
241
242 let with_fallbacks = primary.with_fallbacks(vec![fallback]);
243 let err = with_fallbacks.invoke(5, None).await.unwrap_err();
244 assert!(matches!(err, LcelError::Provider(msg) if msg.contains("openai")));
246 }
247
248 #[tokio::test]
249 async fn multiple_fallbacks_first_wins() {
250 let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
251 Err(LcelError::Other("primary failed".to_string()))
252 });
253 let fb1 = RunnableLambda::new_sync(|x: i32| x + 100);
254 let fb2 = RunnableLambda::new_sync(|x: i32| x + 200);
255
256 let with_fallbacks = primary.with_fallbacks(vec![fb1, fb2]);
257 let result = with_fallbacks.invoke(5, None).await.unwrap();
258 assert_eq!(result, 105); }
260
261 #[tokio::test]
262 async fn first_fallback_fails_second_succeeds() {
263 let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
264 Err(LcelError::Other("primary failed".to_string()))
265 });
266 let fb1 = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
267 Err(LcelError::Other("fb1 failed".to_string()))
268 });
269 let fb2 = RunnableLambda::new_sync(|x: i32| x + 200);
270
271 let with_fallbacks = primary.with_fallbacks(vec![fb1, fb2]);
272 let result = with_fallbacks.invoke(5, None).await.unwrap();
273 assert_eq!(result, 205); }
275
276 #[tokio::test]
277 async fn stream_primary_succeeds() {
278 let primary = RunnableLambda::new_sync(|x: i32| x * 2);
279 let with_fallbacks = primary.with_fallbacks(Vec::<RunnableLambda<i32, i32>>::new());
280
281 let mut stream = with_fallbacks.stream(5, None).await.unwrap();
282 let result = stream.next().await.unwrap().unwrap();
283 assert_eq!(result, 10);
284 }
285
286 #[tokio::test]
287 async fn stream_primary_fails_fallback_succeeds() {
288 let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
289 Err(LcelError::Other("primary failed".to_string()))
290 });
291 let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
292
293 let with_fallbacks = primary.with_fallbacks(vec![fallback]);
294 let mut stream = with_fallbacks.stream(5, None).await.unwrap();
295 let result = stream.next().await.unwrap().unwrap();
296 assert_eq!(result, 15);
297 }
298
299 #[tokio::test]
300 async fn batch_works() {
301 let primary = RunnableLambda::new_sync(|x: i32| x * 2);
302 let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
303
304 let with_fallbacks = primary.with_fallbacks(vec![fallback]);
305 let results = with_fallbacks.batch(vec![1, 2, 3], None).await.unwrap();
306 assert_eq!(results, vec![2, 4, 6]); }
308
309 #[tokio::test]
310 async fn transform_works() {
311 let primary = RunnableLambda::new_sync(|x: i32| x * 2);
312 let with_fallbacks = primary.with_fallbacks(Vec::<RunnableLambda<i32, i32>>::new());
313
314 let input = Box::pin(futures_util::stream::iter(vec![
315 Ok(1i32),
316 Ok(2i32),
317 Ok(3i32),
318 ])) as Pin<Box<dyn Stream<Item = Result<i32, LcelError>> + Send>>;
319
320 let mut output = with_fallbacks.transform(input, None).await.unwrap();
321 let result = output.next().await.unwrap().unwrap();
323 assert_eq!(result, 6);
324 }
325
326 #[tokio::test]
327 async fn debug_format() {
328 let primary = RunnableLambda::new_sync(|x: i32| x);
329 let with_fallbacks = primary.with_fallbacks(Vec::<RunnableLambda<i32, i32>>::new());
330 let debug_str = format!("{:?}", with_fallbacks);
331 assert!(debug_str.contains("RunnableWithFallbacks"));
332 }
333
334 #[tokio::test]
335 async fn pipe_with_fallbacks() {
336 let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
337 Err(LcelError::Other("fail".to_string()))
338 });
339 let fallback = RunnableLambda::new_sync(|x: i32| x + 10);
340
341 let double = RunnableLambda::new_sync(|x: i32| x * 2);
343 let chain = primary.with_fallbacks(vec![fallback]).pipe(double);
344
345 let result = chain.invoke(5, None).await.unwrap();
346 assert_eq!(result, 30); }
348}