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}