Skip to main content

lc_core/runnables/
runnable_trait.rs

1// src/core/runnables/runnable_trait.rs
2//! Runnable trait - foundation of LCEL (LangChain Expression Language).
3//!
4//! Every LangChain component implements Runnable, enabling
5//! chaining, composition, and interoperability.
6
7use super::RunnableConfig;
8use async_trait::async_trait;
9use futures_util::Stream;
10use std::pin::Pin;
11
12/// Base trait for all LangChain components.
13///
14/// This trait defines the core interface every component must implement:
15/// - Single execution via `invoke`
16/// - Batch processing via `batch`
17/// - Streaming output via `stream`
18/// - Stream-to-stream transformation via `transform`
19///
20/// # Example
21/// ```no_run
22/// use lc_core::runnables::Runnable;
23/// use lc_core::runnables::RunnableConfig;
24/// use async_trait::async_trait;
25///
26/// // Define a simple Runnable: add one
27/// struct AddOne;
28///
29/// #[async_trait]
30/// impl Runnable<i32, i32> for AddOne {
31///     type Error = std::convert::Infallible;
32///
33///     async fn invoke(&self, input: i32, _config: Option<RunnableConfig>) -> Result<i32, Self::Error> {
34///         Ok(input + 1)
35///     }
36/// }
37/// ```
38#[async_trait]
39pub trait Runnable<Input: Send + Sync + 'static, Output: Send + Sync + 'static>:
40    Send + Sync
41{
42    /// Error type.
43    type Error: std::error::Error + Send + Sync + 'static;
44
45    /// Transforms single input to output.
46    ///
47    /// This is the primary method for single execution.
48    ///
49    /// # Arguments
50    /// * `input` - Input to process.
51    /// * `config` - Optional execution configuration.
52    ///
53    /// # Returns
54    /// Execution result.
55    async fn invoke(
56        &self,
57        input: Input,
58        config: Option<RunnableConfig>,
59    ) -> Result<Output, Self::Error>;
60
61    /// Batch processing - transforms multiple inputs to outputs.
62    ///
63    /// Default implementation processes inputs concurrently with a bounded
64    /// concurrency: `config.max_concurrency` items run at once (defaults to
65    /// all inputs), and results are returned in input order regardless of
66    /// completion order (`buffered`, not `buffer_unordered`). Override for
67    /// provider-level batch optimization.
68    ///
69    /// # Arguments
70    /// * `inputs` - Input vector.
71    /// * `config` - Optional batch configuration.
72    ///
73    /// # Returns
74    /// Result vector, ordered as the inputs.
75    async fn batch(
76        &self,
77        inputs: Vec<Input>,
78        config: Option<RunnableConfig>,
79    ) -> Result<Vec<Output>, Self::Error> {
80        use futures_util::StreamExt;
81
82        // Concurrency cap: `max_concurrency`, clamped to at least 1 so an
83        // explicit `Some(0)` (or an empty input list) cannot panic `buffered`.
84        let limit = config
85            .as_ref()
86            .and_then(|c| c.max_concurrency)
87            .unwrap_or(inputs.len())
88            .max(1);
89
90        let results = futures_util::stream::iter(inputs)
91            .map(|input| {
92                let config = config.clone();
93                async move { self.invoke(input, config).await }
94            })
95            .buffered(limit)
96            .collect::<Vec<Result<_, _>>>()
97            .await;
98
99        // Short-circuit on the first error, preserving input order otherwise.
100        results.into_iter().collect()
101    }
102
103    /// Streaming output - for real-time responses (LLM, etc).
104    ///
105    /// Enables real-time stream processing of output,
106    /// suitable for chat models, token generation, etc.
107    ///
108    /// # Arguments
109    /// * `input` - Input to process.
110    /// * `config` - Optional configuration.
111    ///
112    /// # Returns
113    /// Output stream.
114    ///
115    /// # Default Implementation
116    /// Wraps invoke result as single-element stream.
117    /// Types supporting true streaming should override.
118    async fn stream(
119        &self,
120        input: Input,
121        config: Option<RunnableConfig>,
122    ) -> Result<Pin<Box<dyn Stream<Item = Result<Output, Self::Error>> + Send>>, Self::Error> {
123        // Default: wrap invoke result as single-element stream
124        // All Runables automatically get stream capability
125        // Types with true streaming (like LLM) should override
126        let result = self.invoke(input, config).await?;
127        let stream = futures_util::stream::once(async move { Ok(result) });
128        Ok(Box::pin(stream))
129    }
130
131    /// Stream-to-stream transformation - the core of LCEL streaming.
132    ///
133    /// Takes an input stream and produces an output stream, enabling
134    /// pipeline streaming without buffering intermediate results.
135    ///
136    /// # Default Implementation
137    /// Drives each input item through `stream` and concatenates the per-item
138    /// streams in order — the LangChain default `transform` semantics. A step
139    /// that overrides `stream` (e.g. an LLM) yields a real token stream per
140    /// item; a step using the default `stream` maps elementwise via `invoke`.
141    /// Components that want aggregation (e.g. incremental parsers) should
142    /// override this method.
143    ///
144    /// # Arguments
145    /// * `input` - Input stream to transform.
146    /// * `config` - Optional execution configuration.
147    ///
148    /// # Returns
149    /// Output stream.
150    async fn transform(
151        &self,
152        input: Pin<Box<dyn Stream<Item = Result<Input, Self::Error>> + Send>>,
153        config: Option<RunnableConfig>,
154    ) -> Result<Pin<Box<dyn Stream<Item = Result<Output, Self::Error>> + Send>>, Self::Error> {
155        use futures_util::StreamExt;
156
157        // Buffer the input items, then run each one through `stream` and
158        // concatenate the per-item streams (elementwise semantics).
159        let mut items = Vec::new();
160        let mut input = input;
161        while let Some(item) = input.next().await {
162            items.push(item?);
163        }
164
165        let mut per_item_streams = Vec::with_capacity(items.len());
166        for item in items {
167            let stream = self.stream(item, config.clone()).await?;
168            per_item_streams.push(stream);
169        }
170
171        let flattened = futures_util::stream::iter(per_item_streams).flatten();
172        Ok(Box::pin(flattened))
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use futures_util::StreamExt;
180
181    struct TestRunnable;
182
183    #[async_trait]
184    impl Runnable<String, String> for TestRunnable {
185        type Error = std::convert::Infallible;
186
187        async fn invoke(
188            &self,
189            input: String,
190            _config: Option<RunnableConfig>,
191        ) -> Result<String, Self::Error> {
192            Ok(format!("processed: {}", input))
193        }
194    }
195
196    #[tokio::test]
197    async fn test_default_stream_returns_single_element() {
198        let runnable = TestRunnable;
199        let mut stream = runnable.stream("test".to_string(), None).await.unwrap();
200
201        let first = stream.next().await;
202        assert!(first.is_some());
203        assert_eq!(first.unwrap().unwrap(), "processed: test");
204
205        let second = stream.next().await;
206        assert!(second.is_none());
207    }
208
209    #[tokio::test]
210    async fn test_invoke_matches_stream_result() {
211        let runnable = TestRunnable;
212
213        let invoke_result = runnable.invoke("hello".to_string(), None).await.unwrap();
214        let mut stream = runnable.stream("hello".to_string(), None).await.unwrap();
215        let stream_result = stream.next().await.unwrap().unwrap();
216
217        assert_eq!(invoke_result, stream_result);
218    }
219
220    #[tokio::test]
221    async fn test_default_transform_maps_elementwise() {
222        let runnable = TestRunnable;
223        let input_stream = Box::pin(futures_util::stream::iter(vec![
224            Ok("first".to_string()),
225            Ok("second".to_string()),
226            Ok("third".to_string()),
227        ]))
228            as Pin<Box<dyn Stream<Item = Result<String, std::convert::Infallible>> + Send>>;
229
230        let mut output_stream = runnable.transform(input_stream, None).await.unwrap();
231
232        // Default transform maps each item through invoke (elementwise).
233        let mut results = Vec::new();
234        while let Some(item) = output_stream.next().await {
235            results.push(item.unwrap());
236        }
237        assert_eq!(
238            results,
239            vec![
240                "processed: first".to_string(),
241                "processed: second".to_string(),
242                "processed: third".to_string(),
243            ]
244        );
245    }
246
247    #[tokio::test]
248    async fn test_default_transform_empty_input() {
249        let runnable = TestRunnable;
250        let input_stream = Box::pin(futures_util::stream::empty::<
251            Result<String, std::convert::Infallible>,
252        >())
253            as Pin<Box<dyn Stream<Item = Result<String, std::convert::Infallible>> + Send>>;
254
255        let mut output_stream = runnable.transform(input_stream, None).await.unwrap();
256
257        // Empty input → empty output
258        assert!(output_stream.next().await.is_none());
259    }
260
261    #[tokio::test]
262    async fn test_default_batch_preserves_order() {
263        let runnable = TestRunnable;
264        let results = runnable
265            .batch(
266                vec!["a".to_string(), "b".to_string(), "c".to_string()],
267                None,
268            )
269            .await
270            .unwrap();
271        assert_eq!(
272            results,
273            vec![
274                "processed: a".to_string(),
275                "processed: b".to_string(),
276                "processed: c".to_string(),
277            ]
278        );
279    }
280
281    #[tokio::test]
282    async fn test_default_batch_respects_max_concurrency() {
283        let runnable = TestRunnable;
284        let config = RunnableConfig::new().with_max_concurrency(1);
285        let results = runnable
286            .batch(
287                vec!["x".to_string(), "y".to_string(), "z".to_string()],
288                Some(config),
289            )
290            .await
291            .unwrap();
292        assert_eq!(
293            results,
294            vec![
295                "processed: x".to_string(),
296                "processed: y".to_string(),
297                "processed: z".to_string(),
298            ]
299        );
300    }
301
302    #[tokio::test]
303    async fn test_default_batch_empty_input() {
304        let runnable = TestRunnable;
305        let results = runnable.batch(vec![], None).await.unwrap();
306        assert!(results.is_empty());
307    }
308}