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(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
76        // Try primary
77        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                // Try each fallback
87                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(e) => {
99                            log::warn!("fallback invocation failed (trying next): {e}");
100                            continue;
101                        }
102                    }
103                }
104                // All failed, return the primary's error
105                log::warn!("primary and all fallbacks failed; returning primary's error");
106                Err(primary_error)
107            }
108        }
109    }
110}
111
112#[async_trait]
113impl<I: Clone + Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O>
114    for RunnableWithFallbacks<I, O>
115{
116    type Error = LcelError;
117
118    /// Try the primary, then fallbacks on failure.
119    async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<O, LcelError> {
120        self.try_all(input, config).await
121    }
122
123    /// Stream: try the primary first. On failure, try fallbacks.
124    /// Returns the first successful stream, or the primary's error.
125    async fn stream(
126        &self,
127        input: I,
128        config: Option<RunnableConfig>,
129    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
130        // Try primary stream
131        let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
132        match self.primary.stream_any(boxed_input, config.clone()).await {
133            Ok(stream) => {
134                let output_stream = stream.map(|result| {
135                    result.and_then(|boxed| {
136                        boxed.downcast::<O>().map(|b| *b).map_err(|_| {
137                            LcelError::TypeMismatch(format!(
138                                "fallback stream downcast: expected {}",
139                                std::any::type_name::<O>()
140                            ))
141                        })
142                    })
143                });
144                return Ok(Box::pin(output_stream));
145            }
146            Err(primary_error) => {
147                // Try each fallback
148                for fallback in &self.fallbacks {
149                    let boxed_input = Box::new(input.clone()) as Box<dyn Any + Send>;
150                    match fallback.stream_any(boxed_input, config.clone()).await {
151                        Ok(stream) => {
152                            let output_stream = stream.map(|result| {
153                                result.and_then(|boxed| {
154                                    boxed.downcast::<O>().map(|b| *b).map_err(|_| {
155                                        LcelError::TypeMismatch(format!(
156                                            "fallback stream downcast: expected {}",
157                                            std::any::type_name::<O>()
158                                        ))
159                                    })
160                                })
161                            });
162                            return Ok(Box::pin(output_stream));
163                        }
164                        Err(e) => {
165                            log::warn!("fallback stream failed (trying next): {e}");
166                            continue;
167                        }
168                    }
169                }
170                log::warn!("primary and all fallback streams failed; returning primary's error");
171                Err(primary_error)
172            }
173        }
174    }
175
176    /// Batch: apply fallback logic per-item.
177    async fn batch(
178        &self,
179        inputs: Vec<I>,
180        config: Option<RunnableConfig>,
181    ) -> Result<Vec<O>, LcelError> {
182        let mut results = Vec::with_capacity(inputs.len());
183        for input in inputs {
184            results.push(self.invoke(input, config.clone()).await?);
185        }
186        Ok(results)
187    }
188
189    /// Transform: 逐项调用 `invoke`(带 fallback),产出与输入等长的流。
190    async fn transform(
191        &self,
192        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
193        config: Option<RunnableConfig>,
194    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send + '_>>, LcelError> {
195        // M4: 旧实现只处理输入流最后一项,其余静默丢弃(与 batch 语义不一致)。
196        // 改为逐项 elementwise:缓冲输入后对每一项调用 invoke-with-fallback,
197        // 一一对应输出(与 trait 默认 transform 的缓冲模式一致,避免返回流借用 self)。
198        let mut items = Vec::new();
199        let mut input = input;
200        while let Some(item) = input.next().await {
201            items.push(item?);
202        }
203        let mut results = Vec::with_capacity(items.len());
204        for item in items {
205            results.push(self.invoke(item, config.clone()).await);
206        }
207        Ok(Box::pin(futures_util::stream::iter(results)))
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::runnables::{RunnableExt, RunnableLambda};
215    use futures_util::StreamExt;
216
217    #[tokio::test]
218    async fn primary_succeeds_no_fallback() {
219        let primary = RunnableLambda::new_sync(|x: i32| x * 2);
220        let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
221
222        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
223        let result = with_fallbacks.invoke(5, None).await.unwrap();
224        assert_eq!(result, 10); // 5 * 2, primary succeeded
225    }
226
227    #[tokio::test]
228    async fn primary_fails_fallback_succeeds() {
229        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
230            Err(LcelError::Other("primary failed".to_string()))
231        });
232        let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
233
234        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
235        let result = with_fallbacks.invoke(5, None).await.unwrap();
236        assert_eq!(result, 15); // 5 * 3, fallback succeeded
237    }
238
239    #[tokio::test]
240    async fn all_fail_returns_primary_error() {
241        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
242            Err(LcelError::Provider("openai timeout".to_string()))
243        });
244        let fallback = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
245            Err(LcelError::Provider("anthropic timeout".to_string()))
246        });
247
248        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
249        let err = with_fallbacks.invoke(5, None).await.unwrap_err();
250        // Should return the primary's error
251        assert!(matches!(err, LcelError::Provider(msg) if msg.contains("openai")));
252    }
253
254    #[tokio::test]
255    async fn multiple_fallbacks_first_wins() {
256        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
257            Err(LcelError::Other("primary failed".to_string()))
258        });
259        let fb1 = RunnableLambda::new_sync(|x: i32| x + 100);
260        let fb2 = RunnableLambda::new_sync(|x: i32| x + 200);
261
262        let with_fallbacks = primary.with_fallbacks(vec![fb1, fb2]);
263        let result = with_fallbacks.invoke(5, None).await.unwrap();
264        assert_eq!(result, 105); // 5 + 100, first fallback succeeded
265    }
266
267    #[tokio::test]
268    async fn first_fallback_fails_second_succeeds() {
269        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
270            Err(LcelError::Other("primary failed".to_string()))
271        });
272        let fb1 = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
273            Err(LcelError::Other("fb1 failed".to_string()))
274        });
275        let fb2 = RunnableLambda::new_sync(|x: i32| x + 200);
276
277        let with_fallbacks = primary.with_fallbacks(vec![fb1, fb2]);
278        let result = with_fallbacks.invoke(5, None).await.unwrap();
279        assert_eq!(result, 205); // 5 + 200, second fallback succeeded
280    }
281
282    #[tokio::test]
283    async fn stream_primary_succeeds() {
284        let primary = RunnableLambda::new_sync(|x: i32| x * 2);
285        let with_fallbacks = primary.with_fallbacks(Vec::<RunnableLambda<i32, i32>>::new());
286
287        let mut stream = with_fallbacks.stream(5, None).await.unwrap();
288        let result = stream.next().await.unwrap().unwrap();
289        assert_eq!(result, 10);
290    }
291
292    #[tokio::test]
293    async fn stream_primary_fails_fallback_succeeds() {
294        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
295            Err(LcelError::Other("primary failed".to_string()))
296        });
297        let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
298
299        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
300        let mut stream = with_fallbacks.stream(5, None).await.unwrap();
301        let result = stream.next().await.unwrap().unwrap();
302        assert_eq!(result, 15);
303    }
304
305    #[tokio::test]
306    async fn batch_works() {
307        let primary = RunnableLambda::new_sync(|x: i32| x * 2);
308        let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
309
310        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
311        let results = with_fallbacks.batch(vec![1, 2, 3], None).await.unwrap();
312        assert_eq!(results, vec![2, 4, 6]); // primary succeeds for all
313    }
314
315    #[tokio::test]
316    async fn transform_works() {
317        let primary = RunnableLambda::new_sync(|x: i32| x * 2);
318        let with_fallbacks = primary.with_fallbacks(Vec::<RunnableLambda<i32, i32>>::new());
319
320        let input = Box::pin(futures_util::stream::iter(vec![
321            Ok(1i32),
322            Ok(2i32),
323            Ok(3i32),
324        ])) as Pin<Box<dyn Stream<Item = Result<i32, LcelError>> + Send>>;
325
326        let mut output = with_fallbacks.transform(input, None).await.unwrap();
327        // M4: 逐项 elementwise 处理(与 batch 语义一致),而非只处理最后一项。
328        let mut results = Vec::new();
329        while let Some(item) = output.next().await {
330            results.push(item.unwrap());
331        }
332        assert_eq!(results, vec![2, 4, 6]);
333    }
334
335    #[tokio::test]
336    async fn transform_fallback_per_item() {
337        // M4: 逐项失败时按项走 fallback。
338        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
339            Err(LcelError::Other("primary failed".to_string()))
340        });
341        let fallback = RunnableLambda::new_sync(|x: i32| x * 3);
342
343        let with_fallbacks = primary.with_fallbacks(vec![fallback]);
344        let input = Box::pin(futures_util::stream::iter(vec![Ok(1i32), Ok(2i32)]))
345            as Pin<Box<dyn Stream<Item = Result<i32, LcelError>> + Send>>;
346
347        let mut output = with_fallbacks.transform(input, None).await.unwrap();
348        let mut results = Vec::new();
349        while let Some(item) = output.next().await {
350            results.push(item.unwrap());
351        }
352        assert_eq!(results, vec![3, 6]);
353    }
354
355    #[tokio::test]
356    async fn debug_format() {
357        let primary = RunnableLambda::new_sync(|x: i32| x);
358        let with_fallbacks = primary.with_fallbacks(Vec::<RunnableLambda<i32, i32>>::new());
359        let debug_str = format!("{:?}", with_fallbacks);
360        assert!(debug_str.contains("RunnableWithFallbacks"));
361    }
362
363    #[tokio::test]
364    async fn pipe_with_fallbacks() {
365        let primary = RunnableLambda::new_sync_fallible(|_x: i32| -> Result<i32, LcelError> {
366            Err(LcelError::Other("fail".to_string()))
367        });
368        let fallback = RunnableLambda::new_sync(|x: i32| x + 10);
369
370        // pipe: (primary | fallback) -> double
371        let double = RunnableLambda::new_sync(|x: i32| x * 2);
372        let chain = primary.with_fallbacks(vec![fallback]).pipe(double);
373
374        let result = chain.invoke(5, None).await.unwrap();
375        assert_eq!(result, 30); // fallback: 5+10=15, double: 15*2=30
376    }
377}