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