Skip to main content

lc_core/runnables/
ext.rs

1// lc-core/src/runnables/ext.rs
2//! Extension trait for Runnable providing LCEL composition methods.
3//!
4//! `RunnableExt` adds the `pipe()` method to any `Runnable` whose `Error`
5//! type can be converted into `LcelError`. This enables the core LCEL
6//! composition pattern:
7//!
8//! ```rust,ignore
9//! let chain = prompt.pipe(llm).pipe(parser);
10//! ```
11
12use super::any::into_runnable_any;
13use super::error::LcelError;
14use super::fallback::RunnableWithFallbacks;
15use super::retry::{RetryConfig, RunnableRetry};
16use super::runnable_trait::Runnable;
17use super::sequence::RunnableSequence;
18
19/// Extension trait that provides LCEL composition methods for `Runnable`.
20///
21/// Automatically implemented for all `Runnable<I, O>` types where
22/// `Self::Error: Into<LcelError>`.
23pub trait RunnableExt<Input: Send + Sync + 'static, Output: Send + Sync + 'static>:
24    Runnable<Input, Output>
25where
26    Self::Error: Into<LcelError>,
27    Self: Sized + 'static,
28{
29    /// Pipe the output of this runnable into another runnable.
30    ///
31    /// This is the core LCEL composition operator. It creates a
32    /// `RunnableSequence` that executes `self` first, then passes
33    /// the output to `other`.
34    ///
35    /// # Type Safety
36    ///
37    /// The compiler ensures that the output type of `self` matches
38    /// the input type of `other`. At runtime, intermediate types
39    /// are erased via `RunnableAny`, but the type relationship
40    /// was already proven at compile time.
41    ///
42    /// # Example
43    ///
44    /// ```rust,ignore
45    /// let chain = prompt.pipe(llm).pipe(parser);
46    /// let result = chain.invoke("What is Rust?".to_string(), None).await?;
47    /// ```
48    fn pipe<O2, R2>(self, other: R2) -> RunnableSequence<Input, O2>
49    where
50        O2: Send + Sync + 'static,
51        R2: Runnable<Output, O2> + Send + Sync + 'static,
52        R2::Error: Into<LcelError>,
53    {
54        RunnableSequence::from_pair(self, other)
55    }
56
57    /// Create a `RunnableSequence` from this runnable as a single step.
58    ///
59    /// Useful when you want to start building a pipeline and add
60    /// steps later via `pipe()`.
61    fn into_sequence(self) -> RunnableSequence<Input, Output> {
62        RunnableSequence::from_single(self)
63    }
64
65    /// Add fallback runnables that are tried if this one fails.
66    ///
67    /// If `self` returns an error, each fallback is tried in order.
68    /// The first successful result is returned. If all fail, the
69    /// primary's error is returned.
70    ///
71    /// The input type `Input` must be `Clone` so that the input can
72    /// be re-boxed for each fallback attempt.
73    ///
74    /// # Example
75    ///
76    /// ```rust,ignore
77    /// let chain = prompt
78    ///     .pipe(openai_llm)
79    ///     .with_fallbacks(vec![anthropic_llm, ollama_llm])
80    ///     .pipe(parser);
81    /// ```
82    fn with_fallbacks<R>(self, fallbacks: Vec<R>) -> RunnableWithFallbacks<Input, Output>
83    where
84        Input: Clone,
85        R: Runnable<Input, Output> + Send + Sync + 'static,
86        R::Error: Into<LcelError>,
87    {
88        let fallback_boxes: Vec<Box<dyn super::any::RunnableAny>> = fallbacks
89            .into_iter()
90            .map(|r| into_runnable_any(r))
91            .collect();
92        RunnableWithFallbacks::new(self, fallback_boxes)
93    }
94
95    /// Wrap this runnable with retry logic using exponential backoff.
96    ///
97    /// On failure, the runnable is retried up to `max_retries` times
98    /// with increasing delays between attempts.
99    ///
100    /// The input type `Input` must be `Clone` so that the input can
101    /// be re-boxed for each retry attempt.
102    ///
103    /// # Example
104    ///
105    /// ```rust,ignore
106    /// use lc_core::runnables::{RetryConfig, RunnableExt};
107    /// use std::time::Duration;
108    ///
109    /// let chain = prompt.pipe(llm).pipe(parser)
110    ///     .with_retry(RetryConfig {
111    ///         max_retries: 3,
112    ///         initial_delay: Duration::from_millis(500),
113    ///         max_delay: Duration::from_secs(10),
114    ///         backoff_multiplier: 2.0,
115    ///         ..Default::default()
116    ///     });
117    /// ```
118    fn with_retry(self, retry_config: RetryConfig) -> RunnableRetry<Input, Output>
119    where
120        Input: Clone,
121    {
122        let runnable_any = into_runnable_any(self);
123        RunnableRetry::new(runnable_any, retry_config)
124    }
125}
126
127// Blanket implementation: all Runnables with compatible Error get RunnableExt
128impl<I, O, R> RunnableExt<I, O> for R
129where
130    I: Send + Sync + 'static,
131    O: Send + Sync + 'static,
132    R: Runnable<I, O>,
133    R::Error: Into<LcelError>,
134    R: Sized + 'static,
135{
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::RunnableConfig;
142    use async_trait::async_trait;
143    use futures_util::StreamExt;
144
145    struct Double;
146
147    #[async_trait]
148    impl Runnable<i32, i32> for Double {
149        type Error = std::convert::Infallible;
150
151        async fn invoke(
152            &self,
153            input: i32,
154            _config: Option<RunnableConfig>,
155        ) -> Result<i32, Self::Error> {
156            Ok(input * 2)
157        }
158    }
159
160    struct AddSuffix;
161
162    #[async_trait]
163    impl Runnable<i32, String> for AddSuffix {
164        type Error = std::convert::Infallible;
165
166        async fn invoke(
167            &self,
168            input: i32,
169            _config: Option<RunnableConfig>,
170        ) -> Result<String, Self::Error> {
171            Ok(format!("result: {}", input))
172        }
173    }
174
175    #[tokio::test]
176    async fn pipe_creates_sequence() {
177        let chain = Double.pipe(AddSuffix);
178        let result = chain.invoke(5, None).await.unwrap();
179        assert_eq!(result, "result: 10");
180    }
181
182    #[tokio::test]
183    async fn pipe_chain_multiple() {
184        // Double → Double → AddSuffix
185        let chain = Double.pipe(Double).pipe(AddSuffix);
186        let result = chain.invoke(3, None).await.unwrap();
187        assert_eq!(result, "result: 12"); // 3 * 2 * 2 = 12
188    }
189
190    #[tokio::test]
191    async fn pipe_stream_works() {
192        let chain = Double.pipe(AddSuffix);
193        let mut stream = chain.stream(5, None).await.unwrap();
194        let result = stream.next().await.unwrap().unwrap();
195        assert_eq!(result, "result: 10");
196    }
197}