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::configurable::{RunnableConfigurable, RunnableConfigurableFields};
14use super::error::LcelError;
15use super::fallback::RunnableWithFallbacks;
16use super::retry::{RetryConfig, RunnableRetry};
17use super::runnable_trait::Runnable;
18use super::sequence::RunnableSequence;
19
20/// Extension trait that provides LCEL composition methods for `Runnable`.
21///
22/// Automatically implemented for all `Runnable<I, O>` types where
23/// `Self::Error: Into<LcelError>`.
24pub trait RunnableExt<Input: Send + Sync + 'static, Output: Send + Sync + 'static>:
25    Runnable<Input, Output>
26where
27    Self::Error: Into<LcelError>,
28    Self: Sized + 'static,
29{
30    /// Pipe the output of this runnable into another runnable.
31    ///
32    /// This is the core LCEL composition operator. It creates a
33    /// `RunnableSequence` that executes `self` first, then passes
34    /// the output to `other`.
35    ///
36    /// # Type Safety
37    ///
38    /// The compiler ensures that the output type of `self` matches
39    /// the input type of `other`. At runtime, intermediate types
40    /// are erased via `RunnableAny`, but the type relationship
41    /// was already proven at compile time.
42    ///
43    /// # Example
44    ///
45    /// ```rust,ignore
46    /// let chain = prompt.pipe(llm).pipe(parser);
47    /// let result = chain.invoke("What is Rust?".to_string(), None).await?;
48    /// ```
49    fn pipe<O2, R2>(self, other: R2) -> RunnableSequence<Input, O2>
50    where
51        O2: Send + Sync + 'static,
52        R2: Runnable<Output, O2> + Send + Sync + 'static,
53        R2::Error: Into<LcelError>,
54    {
55        RunnableSequence::from_pair(self, other)
56    }
57
58    /// Create a `RunnableSequence` from this runnable as a single step.
59    ///
60    /// Useful when you want to start building a pipeline and add
61    /// steps later via `pipe()`.
62    fn into_sequence(self) -> RunnableSequence<Input, Output> {
63        RunnableSequence::from_single(self)
64    }
65
66    /// Add fallback runnables that are tried if this one fails.
67    ///
68    /// If `self` returns an error, each fallback is tried in order.
69    /// The first successful result is returned. If all fail, the
70    /// primary's error is returned.
71    ///
72    /// The input type `Input` must be `Clone` so that the input can
73    /// be re-boxed for each fallback attempt.
74    ///
75    /// # Example
76    ///
77    /// ```rust,ignore
78    /// let chain = prompt
79    ///     .pipe(openai_llm)
80    ///     .with_fallbacks(vec![anthropic_llm, ollama_llm])
81    ///     .pipe(parser);
82    /// ```
83    fn with_fallbacks<R>(self, fallbacks: Vec<R>) -> RunnableWithFallbacks<Input, Output>
84    where
85        Input: Clone,
86        R: Runnable<Input, Output> + Send + Sync + 'static,
87        R::Error: Into<LcelError>,
88    {
89        let fallback_boxes: Vec<Box<dyn super::any::RunnableAny>> = fallbacks
90            .into_iter()
91            .map(|r| into_runnable_any(r))
92            .collect();
93        RunnableWithFallbacks::new(self, fallback_boxes)
94    }
95
96    /// Wrap this runnable with retry logic using exponential backoff.
97    ///
98    /// On failure, the runnable is retried up to `max_retries` times
99    /// with increasing delays between attempts.
100    ///
101    /// The input type `Input` must be `Clone` so that the input can
102    /// be re-boxed for each retry attempt.
103    ///
104    /// # Example
105    ///
106    /// ```rust,ignore
107    /// use lc_core::runnables::{RetryConfig, RunnableExt};
108    /// use std::time::Duration;
109    ///
110    /// let chain = prompt.pipe(llm).pipe(parser)
111    ///     .with_retry(RetryConfig {
112    ///         max_retries: 3,
113    ///         initial_delay: Duration::from_millis(500),
114    ///         max_delay: Duration::from_secs(10),
115    ///         backoff_multiplier: 2.0,
116    ///         ..Default::default()
117    ///     });
118    /// ```
119    fn with_retry(self, retry_config: RetryConfig) -> RunnableRetry<Input, Output>
120    where
121        Input: Clone,
122    {
123        let runnable_any = into_runnable_any(self);
124        RunnableRetry::new(runnable_any, retry_config)
125    }
126
127    /// Route between a default runnable and named alternatives at invoke time.
128    ///
129    /// Rust counterpart of Python LCEL's `Runnable.configurable_alternatives`.
130    /// The selector key `which` is read from `config.configurable`; the value
131    /// must name the `default_key` (→ this runnable) or one of the
132    /// `alternatives`. An unknown value falls back to this runnable.
133    ///
134    /// # Example
135    ///
136    /// ```rust,ignore
137    /// let chain = llm.configurable_alternatives(
138    ///     "provider", "default",
139    ///     vec![("anthropic", anthropic_llm)],
140    /// );
141    /// // invoke(msgs, Some(RunnableConfig::new().with_configurable("provider", json!("anthropic"))))
142    /// ```
143    fn configurable_alternatives<K, R>(
144        self,
145        which: impl Into<String>,
146        default_key: impl Into<String>,
147        alternatives: Vec<(K, R)>,
148    ) -> RunnableConfigurable<Input, Output>
149    where
150        K: Into<String>,
151        R: Runnable<Input, Output> + Send + Sync + 'static,
152        R::Error: Into<LcelError>,
153    {
154        let mut configurable = RunnableConfigurable::new(self, which, default_key);
155        for (name, runnable) in alternatives {
156            configurable = configurable.with_alternative(name, runnable);
157        }
158        configurable
159    }
160
161    /// Override recognized config fields at invoke time from
162    /// `config.configurable` (Python's `Runnable.configurable_fields`).
163    ///
164    /// `temperature` / `max_tokens` in the configurable map are promoted to
165    /// the typed config fields the providers consume; other keys are merged
166    /// into `config.metadata`.
167    ///
168    /// # Example
169    ///
170    /// ```rust,ignore
171    /// let llm = llm.configurable_fields();
172    /// let config = RunnableConfig::new().with_configurable("temperature", json!(0.5));
173    /// llm.invoke(msgs, Some(config)).await?;   // 本次调用采样温度 0.5
174    /// ```
175    fn configurable_fields(self) -> RunnableConfigurableFields<Input, Output> {
176        RunnableConfigurableFields::new(self)
177    }
178}
179
180// Blanket implementation: all Runnables with compatible Error get RunnableExt
181impl<I, O, R> RunnableExt<I, O> for R
182where
183    I: Send + Sync + 'static,
184    O: Send + Sync + 'static,
185    R: Runnable<I, O>,
186    R::Error: Into<LcelError>,
187    R: Sized + 'static,
188{
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use crate::RunnableConfig;
195    use async_trait::async_trait;
196    use futures_util::StreamExt;
197
198    struct Double;
199
200    #[async_trait]
201    impl Runnable<i32, i32> for Double {
202        type Error = std::convert::Infallible;
203
204        async fn invoke(
205            &self,
206            input: i32,
207            _config: Option<RunnableConfig>,
208        ) -> Result<i32, Self::Error> {
209            Ok(input * 2)
210        }
211    }
212
213    struct AddSuffix;
214
215    #[async_trait]
216    impl Runnable<i32, String> for AddSuffix {
217        type Error = std::convert::Infallible;
218
219        async fn invoke(
220            &self,
221            input: i32,
222            _config: Option<RunnableConfig>,
223        ) -> Result<String, Self::Error> {
224            Ok(format!("result: {}", input))
225        }
226    }
227
228    #[tokio::test]
229    async fn pipe_creates_sequence() {
230        let chain = Double.pipe(AddSuffix);
231        let result = chain.invoke(5, None).await.unwrap();
232        assert_eq!(result, "result: 10");
233    }
234
235    #[tokio::test]
236    async fn pipe_chain_multiple() {
237        // Double → Double → AddSuffix
238        let chain = Double.pipe(Double).pipe(AddSuffix);
239        let result = chain.invoke(3, None).await.unwrap();
240        assert_eq!(result, "result: 12"); // 3 * 2 * 2 = 12
241    }
242
243    #[tokio::test]
244    async fn pipe_stream_works() {
245        let chain = Double.pipe(AddSuffix);
246        let mut stream = chain.stream(5, None).await.unwrap();
247        let result = stream.next().await.unwrap().unwrap();
248        assert_eq!(result, "result: 10");
249    }
250}