Skip to main content

lc_core/runnables/
fallback.rs

1// lc-core/src/runnables/fallback.rs
2//! RunnableWithFallbacks - fallback composition for LCEL pipelines.
3//!
4//! `RunnableWithFallbacks` wraps a primary `Runnable` with a list of fallback
5//! runnables. If the primary fails, each fallback is tried in order until one
6//! succeeds. If all fail, the primary's error is returned.
7//!
8//! # Example
9//!
10//! ```rust,ignore
11//! let chain = prompt
12//!     .pipe(openai_llm)
13//!     .with_fallbacks(vec![anthropic_llm, ollama_llm])
14//!     .pipe(parser);
15//! ```
16
17use 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
27/// A `Runnable` that tries fallbacks when the primary fails.
28///
29/// If the primary runnable returns an error on `invoke`, each fallback is
30/// tried in order. The first successful result is returned. If all fail,
31/// the **primary's** error is returned (so the user sees the error from
32/// the runnable they explicitly chose).
33///
34/// The input type `I` must be `Clone` so that the input can be re-boxed
35/// for each fallback attempt.
36pub 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    /// Create a new fallback runnable with the given primary and fallbacks.
56    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    /// Number of fallback runnables.
69    pub fn fallback_count(&self) -> usize {
70        self.fallbacks.len()
71    }
72
73    /// Try all runnables (primary then fallbacks) on the given input.
74    /// Returns the first successful result, or the primary's error if all fail.
75    async fn try_all(
76        &self,
77        input: I,
78        config: Option<RunnableConfig>,
79    ) -> Result<O, LcelError> {
80        // Try primary
81        let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
82        match self.primary.invoke_any(boxed_input, config.clone()).await {
83            Ok(result) => {
84                result
85                    .downcast::<O>()
86                    .map(|b| *b)
87                    .map_err(|_| LcelError::TypeMismatch(format!(
88                        "fallback primary output downcast: expected {}",
89                        std::any::type_name::<O>()
90                    )))
91            }
92            Err(primary_error) => {
93                // Try each fallback
94                for fallback in &self.fallbacks {
95                    let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
96                    match fallback.invoke_any(boxed_input, config.clone()).await {
97                        Ok(result) => {
98                            return result
99                                .downcast::<O>()
100                                .map(|b| *b)
101                                .map_err(|_| LcelError::TypeMismatch(format!(
102                                    "fallback output downcast: expected {}",
103                                    std::any::type_name::<O>()
104                                )));
105                        }
106                        Err(_) => continue,
107                    }
108                }
109                // All failed, return the primary's error
110                Err(primary_error)
111            }
112        }
113    }
114}
115
116#[async_trait]
117impl<I: Clone + Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O>
118    for RunnableWithFallbacks<I, O>
119{
120    type Error = LcelError;
121
122    /// Try the primary, then fallbacks on failure.
123    async fn invoke(
124        &self,
125        input: I,
126        config: Option<RunnableConfig>,
127    ) -> Result<O, LcelError> {
128        self.try_all(input, config).await
129    }
130
131    /// Stream: try the primary first. On failure, try fallbacks.
132    /// Returns the first successful stream, or the primary's error.
133    async fn stream(
134        &self,
135        input: I,
136        config: Option<RunnableConfig>,
137    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
138        // Try primary stream
139        let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
140        match self.primary.stream_any(boxed_input, config.clone()).await {
141            Ok(stream) => {
142                let output_stream = stream.map(|result| {
143                    result.and_then(|boxed| {
144                        boxed.downcast::<O>().map(|b| *b).map_err(|_| {
145                            LcelError::TypeMismatch(format!(
146                                "fallback stream downcast: expected {}",
147                                std::any::type_name::<O>()
148                            ))
149                        })
150                    })
151                });
152                return Ok(Box::pin(output_stream));
153            }
154            Err(primary_error) => {
155                // Try each fallback
156                for fallback in &self.fallbacks {
157                    let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
158                    match fallback.stream_any(boxed_input, config.clone()).await {
159                        Ok(stream) => {
160                            let output_stream = stream.map(|result| {
161                                result.and_then(|boxed| {
162                                    boxed.downcast::<O>().map(|b| *b).map_err(|_| {
163                                        LcelError::TypeMismatch(format!(
164                                            "fallback stream downcast: expected {}",
165                                            std::any::type_name::<O>()
166                                        ))
167                                    })
168                                })
169                            });
170                            return Ok(Box::pin(output_stream));
171                        }
172                        Err(_) => continue,
173                    }
174                }
175                Err(primary_error)
176            }
177        }
178    }
179
180    /// Batch: apply fallback logic per-item.
181    async fn batch(
182        &self,
183        inputs: Vec<I>,
184        config: Option<RunnableConfig>,
185    ) -> Result<Vec<O>, LcelError> {
186        let mut results = Vec::with_capacity(inputs.len());
187        for input in inputs {
188            results.push(self.invoke(input, config.clone()).await?);
189        }
190        Ok(results)
191    }
192
193    /// Transform: use invoke with fallback, wrap as stream.
194    async fn transform(
195        &self,
196        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
197        config: Option<RunnableConfig>,
198    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
199        // Default: buffer all input, take the last item, invoke with fallback
200        let mut items = Vec::new();
201        let mut input = input;
202        while let Some(item) = input.next().await {
203            items.push(item?);
204        }
205
206        if let Some(last) = items.into_iter().last() {
207            let result = self.invoke(last, config).await?;
208            Ok(Box::pin(futures_util::stream::once(async move { Ok(result) })))
209        } else {
210            Ok(Box::pin(futures_util::stream::empty()))
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::runnables::{RunnableExt, RunnableLambda};
219    use futures_util::StreamExt;
220
221    #[tokio::test]
222    async fn primary_succeeds_no_fallback() {
223        let primary = RunnableLambda::new_sync(|x: i32| x * 2);
224        let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
225
226        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
227        let result = with_fallbacks.invoke(5, None).await.unwrap();
228        assert_eq!(result, 10); // 5 * 2, primary succeeded
229    }
230
231    #[tokio::test]
232    async fn primary_fails_fallback_succeeds() {
233        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
234            Err(LcelError::Other("primary failed".to_string()))
235        });
236        let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
237
238        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
239        let result = with_fallbacks.invoke(5, None).await.unwrap();
240        assert_eq!(result, 15); // 5 * 3, fallback succeeded
241    }
242
243    #[tokio::test]
244    async fn all_fail_returns_primary_error() {
245        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
246            Err(LcelError::Provider("openai timeout".to_string()))
247        });
248        let fallback = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
249            Err(LcelError::Provider("anthropic timeout".to_string()))
250        });
251
252        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
253        let err = with_fallbacks.invoke(5, None).await.unwrap_err();
254        // Should return the primary's error
255        assert!(matches!(err, LcelError::Provider(msg) if msg.contains("openai")));
256    }
257
258    #[tokio::test]
259    async fn multiple_fallbacks_first_wins() {
260        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
261            Err(LcelError::Other("primary failed".to_string()))
262        });
263        let fb1 = RunnableLambda::new_sync(|x: i32| x + 100);
264        let fb2 = RunnableLambda::new_sync(|x: i32| x + 200);
265
266        let with_fallbacks = primary.with_fallbacks(vec![fb1, fb2]);
267        let result = with_fallbacks.invoke(5, None).await.unwrap();
268        assert_eq!(result, 105); // 5 + 100, first fallback succeeded
269    }
270
271    #[tokio::test]
272    async fn first_fallback_fails_second_succeeds() {
273        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
274            Err(LcelError::Other("primary failed".to_string()))
275        });
276        let fb1 = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
277            Err(LcelError::Other("fb1 failed".to_string()))
278        });
279        let fb2 = RunnableLambda::new_sync(|x: i32| x + 200);
280
281        let with_fallbacks = primary.with_fallbacks(vec![fb1, fb2]);
282        let result = with_fallbacks.invoke(5, None).await.unwrap();
283        assert_eq!(result, 205); // 5 + 200, second fallback succeeded
284    }
285
286    #[tokio::test]
287    async fn stream_primary_succeeds() {
288        let primary = RunnableLambda::new_sync(|x: i32| x * 2);
289        let with_fallbacks = primary.with_fallbacks(Vec::<RunnableLambda<i32, i32>>::new());
290
291        let mut stream = with_fallbacks.stream(5, None).await.unwrap();
292        let result = stream.next().await.unwrap().unwrap();
293        assert_eq!(result, 10);
294    }
295
296    #[tokio::test]
297    async fn stream_primary_fails_fallback_succeeds() {
298        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
299            Err(LcelError::Other("primary failed".to_string()))
300        });
301        let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
302
303        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
304        let mut stream = with_fallbacks.stream(5, None).await.unwrap();
305        let result = stream.next().await.unwrap().unwrap();
306        assert_eq!(result, 15);
307    }
308
309    #[tokio::test]
310    async fn batch_works() {
311        let primary = RunnableLambda::new_sync(|x: i32| x * 2);
312        let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
313
314        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
315        let results = with_fallbacks.batch(vec![1, 2, 3], None).await.unwrap();
316        assert_eq!(results, vec![2, 4, 6]); // primary succeeds for all
317    }
318
319    #[tokio::test]
320    async fn transform_works() {
321        let primary = RunnableLambda::new_sync(|x: i32| x * 2);
322        let with_fallbacks = primary.with_fallbacks(Vec::<RunnableLambda<i32, i32>>::new());
323
324        let input = Box::pin(futures_util::stream::iter(vec![
325            Ok(1i32),
326            Ok(2i32),
327            Ok(3i32),
328        ])) as Pin<Box<dyn Stream<Item = Result<i32, LcelError>> + Send>>;
329
330        let mut output = with_fallbacks.transform(input, None).await.unwrap();
331        // Default transform takes the last item and invokes: 3 * 2 = 6
332        let result = output.next().await.unwrap().unwrap();
333        assert_eq!(result, 6);
334    }
335
336    #[tokio::test]
337    async fn debug_format() {
338        let primary = RunnableLambda::new_sync(|x: i32| x);
339        let with_fallbacks = primary.with_fallbacks(Vec::<RunnableLambda<i32, i32>>::new());
340        let debug_str = format!("{:?}", with_fallbacks);
341        assert!(debug_str.contains("RunnableWithFallbacks"));
342    }
343
344    #[tokio::test]
345    async fn pipe_with_fallbacks() {
346        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
347            Err(LcelError::Other("fail".to_string()))
348        });
349        let fallback = RunnableLambda::new_sync(|x: i32| x + 10);
350
351        // pipe: (primary | fallback) -> double
352        let double = RunnableLambda::new_sync(|x: i32| x * 2);
353        let chain = primary.with_fallbacks(vec![fallback]).pipe(double);
354
355        let result = chain.invoke(5, None).await.unwrap();
356        assert_eq!(result, 30); // fallback: 5+10=15, double: 15*2=30
357    }
358}