Skip to main content

lc_core/runnables/
sequence.rs

1// lc-core/src/runnables/sequence.rs
2//! RunnableSequence - the core LCEL pipeline type.
3//!
4//! A `RunnableSequence<I, O>` chains multiple `Runnable` steps together,
5//! where the output of each step feeds into the input of the next.
6//! Internally, steps are stored as `Box<dyn RunnableAny>` (type-erased),
7//! but the `I` and `O` type parameters preserve the pipeline's
8//! input and output types at compile time.
9
10use super::any::{into_runnable_any, RunnableAny};
11use super::config::RunnableConfig;
12use super::error::LcelError;
13use super::runnable_trait::Runnable;
14use async_trait::async_trait;
15use futures_util::{Stream, StreamExt};
16use std::any::Any;
17use std::marker::PhantomData;
18use std::pin::Pin;
19
20/// A sequence of `Runnable` steps composed into a pipeline.
21///
22/// Created via the `pipe()` method on `RunnableExt`, or directly
23/// with `from_single` / `from_pair`.
24///
25/// # Type Safety
26///
27/// The `I` and `O` type parameters represent the pipeline's overall
28/// input and output types. Intermediate types are erased at runtime
29/// via `RunnableAny`, but the compiler guarantees type compatibility
30/// at each `pipe()` call site.
31///
32/// # Flattening
33///
34/// When two `RunnableSequence` values are piped together, their
35/// internal steps are merged (flattened) rather than nested,
36/// avoiding unnecessary indirection.
37pub struct RunnableSequence<I: Send + Sync + 'static, O: Send + Sync + 'static> {
38    steps: Vec<Box<dyn RunnableAny>>,
39    _marker: PhantomData<(I, O)>,
40}
41
42impl<I: Send + Sync + 'static, O: Send + Sync + 'static> std::fmt::Debug for RunnableSequence<I, O> {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        f.debug_struct("RunnableSequence")
45            .field("steps", &self.steps.len())
46            .field("input", &std::any::type_name::<I>())
47            .field("output", &std::any::type_name::<O>())
48            .finish()
49    }
50}
51
52impl<I: Send + Sync + 'static, O: Send + Sync + 'static> RunnableSequence<I, O> {
53    /// Create a sequence from a single runnable step.
54    pub fn from_single<R>(runnable: R) -> Self
55    where
56        R: Runnable<I, O> + 'static,
57        R::Error: Into<LcelError>,
58    {
59        Self {
60            steps: vec![into_runnable_any(runnable)],
61            _marker: PhantomData,
62        }
63    }
64
65    /// Create a sequence from two runnable steps.
66    ///
67    /// The output type of the first must match the input type of the second.
68    pub fn from_pair<R1, R2, M>(first: R1, second: R2) -> RunnableSequence<I, O>
69    where
70        M: Send + Sync + 'static,
71        R1: Runnable<I, M> + 'static,
72        R1::Error: Into<LcelError>,
73        R2: Runnable<M, O> + 'static,
74        R2::Error: Into<LcelError>,
75    {
76        Self {
77            steps: vec![into_runnable_any(first), into_runnable_any(second)],
78            _marker: PhantomData,
79        }
80    }
81
82    /// Append a step to this sequence, returning a new sequence
83    /// with the updated output type.
84    pub fn pipe<O2, R>(self, other: R) -> RunnableSequence<I, O2>
85    where
86        O2: Send + Sync + 'static,
87        R: Runnable<O, O2> + Send + Sync + 'static,
88        R::Error: Into<LcelError>,
89    {
90        let mut steps = self.steps;
91        steps.push(into_runnable_any(other));
92        RunnableSequence {
93            steps,
94            _marker: PhantomData,
95        }
96    }
97
98    /// Number of steps in this sequence.
99    pub fn len(&self) -> usize {
100        self.steps.len()
101    }
102
103    /// Whether this sequence has no steps.
104    pub fn is_empty(&self) -> bool {
105        self.steps.is_empty()
106    }
107
108    /// Access the steps as a slice.
109    pub fn steps(&self) -> &[Box<dyn RunnableAny>] {
110        &self.steps
111    }
112}
113
114#[async_trait]
115impl<I: Send + Sync + 'static, O: Send + Sync + 'static> Runnable<I, O> for RunnableSequence<I, O> {
116    type Error = LcelError;
117
118    /// Execute the pipeline: feed input through each step sequentially.
119    async fn invoke(
120        &self,
121        input: I,
122        config: Option<RunnableConfig>,
123    ) -> Result<O, LcelError> {
124        let mut current: Box<dyn Any + Send> = Box::new(input);
125        for step in &self.steps {
126            current = step.invoke_any(current, config.clone()).await?;
127        }
128        current
129            .downcast::<O>()
130            .map(|b| *b)
131            .map_err(|_| LcelError::TypeMismatch(format!(
132                "final downcast failed: expected {}",
133                std::any::type_name::<O>()
134            )))
135    }
136
137    /// Batch processing: each step processes all inputs before
138    /// passing to the next step. This allows LLM providers to
139    /// optimize batch requests.
140    async fn batch(
141        &self,
142        inputs: Vec<I>,
143        config: Option<RunnableConfig>,
144    ) -> Result<Vec<O>, LcelError> {
145        let mut current: Vec<Box<dyn Any + Send>> =
146            inputs.into_iter().map(|i| Box::new(i) as Box<dyn Any + Send>).collect();
147
148        for step in &self.steps {
149            current = step.batch_any(current, config.clone()).await?;
150        }
151
152        current
153            .into_iter()
154            .map(|boxed| {
155                boxed
156                    .downcast::<O>()
157                    .map(|b| *b)
158                    .map_err(|_| LcelError::TypeMismatch(format!(
159                        "batch final downcast: expected {}",
160                        std::any::type_name::<O>()
161                    )))
162            })
163            .collect()
164    }
165
166    /// Streaming: use `transform` to chain steps as stream-to-stream
167    /// transformations, enabling true pipeline streaming.
168    async fn stream(
169        &self,
170        input: I,
171        config: Option<RunnableConfig>,
172    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
173        // Start with a single-element stream containing the input
174        let input_stream: Pin<Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>> =
175            Box::pin(futures_util::stream::once(async {
176                Ok(Box::new(input) as Box<dyn Any + Send>)
177            }));
178
179        let mut current_stream = input_stream;
180
181        // Chain each step's transform
182        for step in &self.steps {
183            current_stream = step.transform_any(current_stream, config.clone()).await?;
184        }
185
186        // Downcast the final stream from Any to O
187        let output_stream = current_stream.map(|result| {
188            result.and_then(|boxed| {
189                boxed
190                    .downcast::<O>()
191                    .map(|b| *b)
192                    .map_err(|_| LcelError::TypeMismatch(format!(
193                        "stream final downcast: expected {}",
194                        std::any::type_name::<O>()
195                    )))
196            })
197        });
198
199        Ok(Box::pin(output_stream))
200    }
201
202    /// Transform: chain each step's transform to enable
203    /// stream-to-stream pipeline processing.
204    async fn transform(
205        &self,
206        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
207        config: Option<RunnableConfig>,
208    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
209        // Upcast input stream from I to Any
210        let mut current_stream: Pin<Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>> =
211            Box::pin(input.map(|result| {
212                result.map(|item| Box::new(item) as Box<dyn Any + Send>)
213            }));
214
215        // Chain each step's transform
216        for step in &self.steps {
217            current_stream = step.transform_any(current_stream, config.clone()).await?;
218        }
219
220        // Downcast the final stream from Any to O
221        let output_stream = current_stream.map(|result| {
222            result.and_then(|boxed| {
223                boxed
224                    .downcast::<O>()
225                    .map(|b| *b)
226                    .map_err(|_| LcelError::TypeMismatch(format!(
227                        "transform final downcast: expected {}",
228                        std::any::type_name::<O>()
229                    )))
230            })
231        });
232
233        Ok(Box::pin(output_stream))
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use futures_util::StreamExt;
241
242    struct Double;
243
244    #[async_trait]
245    impl Runnable<i32, i32> for Double {
246        type Error = std::convert::Infallible;
247
248        async fn invoke(&self, input: i32, _config: Option<RunnableConfig>) -> Result<i32, Self::Error> {
249            Ok(input * 2)
250        }
251    }
252
253    struct AddOne;
254
255    #[async_trait]
256    impl Runnable<i32, i32> for AddOne {
257        type Error = std::convert::Infallible;
258
259        async fn invoke(&self, input: i32, _config: Option<RunnableConfig>) -> Result<i32, Self::Error> {
260            Ok(input + 1)
261        }
262    }
263
264    struct I32ToString;
265
266    #[async_trait]
267    impl Runnable<i32, String> for I32ToString {
268        type Error = std::convert::Infallible;
269
270        async fn invoke(&self, input: i32, _config: Option<RunnableConfig>) -> Result<String, Self::Error> {
271            Ok(format!("value={}", input))
272        }
273    }
274
275    #[tokio::test]
276    async fn invoke_two_steps() {
277        // Double → AddOne: 5 * 2 + 1 = 11
278        let seq = RunnableSequence::from_pair(Double, AddOne);
279        let result = seq.invoke(5, None).await.unwrap();
280        assert_eq!(result, 11);
281    }
282
283    #[tokio::test]
284    async fn invoke_three_steps() {
285        // Double → AddOne → I32ToString: 3 * 2 + 1 = "value=7"
286        let seq = RunnableSequence::from_pair(Double, AddOne).pipe(I32ToString);
287        let result = seq.invoke(3, None).await.unwrap();
288        assert_eq!(result, "value=7");
289    }
290
291    #[tokio::test]
292    async fn batch_works() {
293        let seq = RunnableSequence::from_pair(Double, AddOne);
294        let results = seq.batch(vec![1, 2, 3], None).await.unwrap();
295        assert_eq!(results, vec![3, 5, 7]);
296    }
297
298    #[tokio::test]
299    async fn stream_works() {
300        let seq = RunnableSequence::from_pair(Double, AddOne);
301        let mut stream = seq.stream(10, None).await.unwrap();
302        let result = stream.next().await.unwrap().unwrap();
303        assert_eq!(result, 21);
304    }
305
306    #[tokio::test]
307    async fn transform_works() {
308        let seq = RunnableSequence::from_pair(Double, AddOne);
309        let input = Box::pin(futures_util::stream::iter(vec![
310            Ok(1i32),
311            Ok(2i32),
312            Ok(3i32),
313        ])) as Pin<Box<dyn Stream<Item = Result<i32, LcelError>> + Send>>;
314
315        let mut output = seq.transform(input, None).await.unwrap();
316        // Default transform buffers and takes last: 3 * 2 + 1 = 7
317        let result = output.next().await.unwrap().unwrap();
318        assert_eq!(result, 7);
319    }
320
321    #[tokio::test]
322    async fn from_single_works() {
323        let seq: RunnableSequence<i32, i32> = RunnableSequence::from_single(Double);
324        let result = seq.invoke(4, None).await.unwrap();
325        assert_eq!(result, 8);
326    }
327
328    #[tokio::test]
329    async fn pipe_on_sequence_works() {
330        let seq = RunnableSequence::from_single(Double).pipe(AddOne).pipe(I32ToString);
331        let result = seq.invoke(5, None).await.unwrap();
332        assert_eq!(result, "value=11"); // 5*2+1=11
333    }
334
335    #[tokio::test]
336    async fn len_and_empty() {
337        let seq = RunnableSequence::from_pair(Double, AddOne);
338        assert_eq!(seq.len(), 2);
339        assert!(!seq.is_empty());
340    }
341}