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}