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: the first step runs through `stream_any` (single input →
165    /// item stream) and every following step runs through `transform_any`.
166    /// Steps that override `stream` (e.g. LLMs) therefore emit a real token
167    /// stream instead of being collapsed to a single `invoke`.
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        if self.steps.is_empty() {
174            return Ok(Box::pin(futures_util::stream::empty()));
175        }
176
177        let mut steps = self.steps.iter();
178        let first = steps.next().expect("steps is non-empty");
179
180        let input_boxed: Box<dyn Any + Send> = Box::new(input);
181        let mut current_stream = first.stream_any(input_boxed, config.clone()).await?;
182
183        // Chain each subsequent step's transform
184        for step in steps {
185            current_stream = step.transform_any(current_stream, config.clone()).await?;
186        }
187
188        // Downcast the final stream from Any to O
189        let output_stream = current_stream.map(|result| {
190            result.and_then(|boxed| {
191                boxed.downcast::<O>().map(|b| *b).map_err(|_| {
192                    LcelError::TypeMismatch(format!(
193                        "stream final downcast: expected {}",
194                        std::any::type_name::<O>()
195                    ))
196                })
197            })
198        });
199
200        Ok(Box::pin(output_stream))
201    }
202
203    /// Transform: chain each step's transform to enable
204    /// stream-to-stream pipeline processing.
205    async fn transform(
206        &self,
207        input: Pin<Box<dyn Stream<Item = Result<I, LcelError>> + Send>>,
208        config: Option<RunnableConfig>,
209    ) -> Result<Pin<Box<dyn Stream<Item = Result<O, LcelError>> + Send>>, LcelError> {
210        // Upcast input stream from I to Any
211        let mut current_stream: Pin<
212            Box<dyn Stream<Item = Result<Box<dyn Any + Send>, LcelError>> + Send>,
213        > = Box::pin(input.map(|result| result.map(|item| Box::new(item) as Box<dyn Any + Send>)));
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.downcast::<O>().map(|b| *b).map_err(|_| {
224                    LcelError::TypeMismatch(format!(
225                        "transform final downcast: expected {}",
226                        std::any::type_name::<O>()
227                    ))
228                })
229            })
230        });
231
232        Ok(Box::pin(output_stream))
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use futures_util::StreamExt;
240
241    struct Double;
242
243    #[async_trait]
244    impl Runnable<i32, i32> for Double {
245        type Error = std::convert::Infallible;
246
247        async fn invoke(
248            &self,
249            input: i32,
250            _config: Option<RunnableConfig>,
251        ) -> Result<i32, Self::Error> {
252            Ok(input * 2)
253        }
254    }
255
256    struct AddOne;
257
258    #[async_trait]
259    impl Runnable<i32, i32> for AddOne {
260        type Error = std::convert::Infallible;
261
262        async fn invoke(
263            &self,
264            input: i32,
265            _config: Option<RunnableConfig>,
266        ) -> Result<i32, Self::Error> {
267            Ok(input + 1)
268        }
269    }
270
271    struct I32ToString;
272
273    #[async_trait]
274    impl Runnable<i32, String> for I32ToString {
275        type Error = std::convert::Infallible;
276
277        async fn invoke(
278            &self,
279            input: i32,
280            _config: Option<RunnableConfig>,
281        ) -> Result<String, Self::Error> {
282            Ok(format!("value={}", input))
283        }
284    }
285
286    #[tokio::test]
287    async fn invoke_two_steps() {
288        // Double → AddOne: 5 * 2 + 1 = 11
289        let seq = RunnableSequence::from_pair(Double, AddOne);
290        let result = seq.invoke(5, None).await.unwrap();
291        assert_eq!(result, 11);
292    }
293
294    #[tokio::test]
295    async fn invoke_three_steps() {
296        // Double → AddOne → I32ToString: 3 * 2 + 1 = "value=7"
297        let seq = RunnableSequence::from_pair(Double, AddOne).pipe(I32ToString);
298        let result = seq.invoke(3, None).await.unwrap();
299        assert_eq!(result, "value=7");
300    }
301
302    #[tokio::test]
303    async fn batch_works() {
304        let seq = RunnableSequence::from_pair(Double, AddOne);
305        let results = seq.batch(vec![1, 2, 3], None).await.unwrap();
306        assert_eq!(results, vec![3, 5, 7]);
307    }
308
309    #[tokio::test]
310    async fn stream_works() {
311        let seq = RunnableSequence::from_pair(Double, AddOne);
312        let mut stream = seq.stream(10, None).await.unwrap();
313        let result = stream.next().await.unwrap().unwrap();
314        assert_eq!(result, 21);
315    }
316
317    #[tokio::test]
318    async fn transform_works_elementwise() {
319        let seq = RunnableSequence::from_pair(Double, AddOne);
320        let input = Box::pin(futures_util::stream::iter(vec![
321            Ok(1i32),
322            Ok(2i32),
323            Ok(3i32),
324        ])) as Pin<Box<dyn Stream<Item = Result<i32, LcelError>> + Send>>;
325
326        // Default transform maps each item through the chain elementwise
327        // (LangChain default semantics): 1→3, 2→5, 3→7.
328        let mut output = seq.transform(input, None).await.unwrap();
329        let mut results = Vec::new();
330        while let Some(item) = output.next().await {
331            results.push(item.unwrap());
332        }
333        assert_eq!(results, vec![3, 5, 7]);
334    }
335
336    // A runnable that overrides `stream` to emit several items, mimicking an
337    // LLM token stream. `invoke` returns a distinguishable value so tests can
338    // prove the streaming path was actually taken.
339    struct StreamingTokenLLM;
340
341    #[async_trait]
342    impl Runnable<i32, i32> for StreamingTokenLLM {
343        type Error = std::convert::Infallible;
344
345        async fn invoke(
346            &self,
347            input: i32,
348            _config: Option<RunnableConfig>,
349        ) -> Result<i32, Self::Error> {
350            Ok(input * 1000)
351        }
352
353        async fn stream(
354            &self,
355            input: i32,
356            _config: Option<RunnableConfig>,
357        ) -> Result<Pin<Box<dyn Stream<Item = Result<i32, Self::Error>> + Send>>, Self::Error>
358        {
359            let stream =
360                futures_util::stream::iter(vec![Ok(input), Ok(input + 100), Ok(input + 200)]);
361            Ok(Box::pin(stream))
362        }
363    }
364
365    #[tokio::test]
366    async fn stream_uses_real_streaming_for_first_step() {
367        // Single step with a real `stream` override: `sequence.stream` must
368        // emit every streamed item (proving it goes through `stream_any`, not
369        // a single `invoke`).
370        let seq: RunnableSequence<i32, i32> = RunnableSequence::from_single(StreamingTokenLLM);
371        let mut stream = seq.stream(5, None).await.unwrap();
372        let mut results = Vec::new();
373        while let Some(item) = stream.next().await {
374            results.push(item.unwrap());
375        }
376        assert_eq!(results, vec![5, 105, 205]);
377    }
378
379    #[tokio::test]
380    async fn stream_chains_subsequent_steps_elementwise() {
381        // StreamingTokenLLM → AddOne: the token stream [5, 105, 205] is
382        // transformed elementwise by AddOne → [6, 106, 206].
383        let seq = RunnableSequence::from_pair(StreamingTokenLLM, AddOne);
384        let mut stream = seq.stream(5, None).await.unwrap();
385        let mut results = Vec::new();
386        while let Some(item) = stream.next().await {
387            results.push(item.unwrap());
388        }
389        assert_eq!(results, vec![6, 106, 206]);
390    }
391
392    #[tokio::test]
393    async fn from_single_works() {
394        let seq: RunnableSequence<i32, i32> = RunnableSequence::from_single(Double);
395        let result = seq.invoke(4, None).await.unwrap();
396        assert_eq!(result, 8);
397    }
398
399    #[tokio::test]
400    async fn pipe_on_sequence_works() {
401        let seq = RunnableSequence::from_single(Double)
402            .pipe(AddOne)
403            .pipe(I32ToString);
404        let result = seq.invoke(5, None).await.unwrap();
405        assert_eq!(result, "value=11"); // 5*2+1=11
406    }
407
408    #[tokio::test]
409    async fn len_and_empty() {
410        let seq = RunnableSequence::from_pair(Double, AddOne);
411        assert_eq!(seq.len(), 2);
412        assert!(!seq.is_empty());
413    }
414}