Skip to main content

lc_core/runnables/
parallel.rs

1// lc-core/src/runnables/parallel.rs
2//! RunnableParallel - fan-out/fan-in composition.
3//!
4//! `RunnableParallel` runs multiple `Runnable` steps concurrently on
5//! the same input, collecting results into a `HashMap<String, Value>`.
6//! This is the LCEL equivalent of Python's `RunnableParallel` / `RunnableMap`.
7
8use super::config::RunnableConfig;
9use super::error::LcelError;
10use super::runnable_trait::Runnable;
11use async_trait::async_trait;
12use futures_util::Stream;
13use serde_json::Value;
14use std::collections::HashMap;
15use std::pin::Pin;
16use std::sync::Arc;
17
18/// A `Runnable` that runs multiple steps in parallel on the same input.
19///
20/// Each step is identified by a string key. The output is a
21/// `HashMap<String, Value>` where each key maps to the corresponding
22/// step's output (serialized as `serde_json::Value`).
23///
24/// # Example
25///
26/// ```rust,ignore
27/// let parallel = RunnableParallel::<String>::new()
28///     .with("length", RunnableLambda::new_sync(|s: String| s.len() as i64))
29///     .with("upper", RunnableLambda::new_sync(|s: String| s.to_uppercase()));
30///
31/// let result = parallel.invoke("hello".to_string(), None).await?;
32/// // result = {"length": 5, "upper": "HELLO"}
33/// ```
34pub struct RunnableParallel<I: Send + Sync + 'static> {
35    steps: Vec<(String, Arc<dyn ParallelStep<I>>)>,
36}
37
38impl<I: Send + Sync + 'static> std::fmt::Debug for RunnableParallel<I> {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        let keys: Vec<&str> = self.steps.iter().map(|(k, _)| k.as_str()).collect();
41        f.debug_struct("RunnableParallel")
42            .field("steps", &keys)
43            .field("input", &std::any::type_name::<I>())
44            .finish()
45    }
46}
47
48impl<I: Clone + Send + Sync + 'static> Default for RunnableParallel<I> {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl<I: Clone + Send + Sync + 'static> RunnableParallel<I> {
55    /// Create an empty parallel runnable.
56    pub fn new() -> Self {
57        Self { steps: Vec::new() }
58    }
59
60    /// Add a step with the given key.
61    ///
62    /// The step's output will be serialized to `serde_json::Value`
63    /// and stored under the key in the output HashMap.
64    pub fn with<O, R>(mut self, key: &str, runnable: R) -> Self
65    where
66        O: serde::Serialize + Send + Sync + 'static,
67        R: Runnable<I, O> + Send + Sync + 'static,
68        R::Error: Into<LcelError>,
69    {
70        self.steps.push((
71            key.to_string(),
72            Arc::new(ParallelStepImpl {
73                inner: runnable,
74                serialize: |output: &O| serde_json::to_value(output),
75                _marker: std::marker::PhantomData,
76            }),
77        ));
78        self
79    }
80
81    /// Number of parallel steps.
82    pub fn len(&self) -> usize {
83        self.steps.len()
84    }
85
86    /// Whether there are no steps.
87    pub fn is_empty(&self) -> bool {
88        self.steps.is_empty()
89    }
90}
91
92/// Trait for a single parallel step that produces a `serde_json::Value`.
93#[async_trait]
94trait ParallelStep<I: Send + Sync + 'static>: Send + Sync {
95    async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<Value, LcelError>;
96}
97
98/// Concrete implementation of `ParallelStep` for any `Runnable<I, O>`.
99struct ParallelStepImpl<I, O, R>
100where
101    I: Send + Sync + 'static,
102    O: serde::Serialize + Send + Sync + 'static,
103    R: Runnable<I, O>,
104{
105    inner: R,
106    serialize: fn(&O) -> Result<Value, serde_json::Error>,
107    _marker: std::marker::PhantomData<I>,
108}
109
110#[async_trait]
111impl<I, O, R> ParallelStep<I> for ParallelStepImpl<I, O, R>
112where
113    I: Clone + Send + Sync + 'static,
114    O: serde::Serialize + Send + Sync + 'static,
115    R: Runnable<I, O>,
116    R::Error: Into<LcelError>,
117{
118    async fn invoke(&self, input: I, config: Option<RunnableConfig>) -> Result<Value, LcelError> {
119        let result = self.inner.invoke(input, config).await.map_err(Into::into)?;
120        (self.serialize)(&result).map_err(|e| LcelError::Other(format!("parallel serialization: {}", e)))
121    }
122}
123
124#[async_trait]
125impl<I: Clone + Send + Sync + 'static> Runnable<I, HashMap<String, Value>> for RunnableParallel<I> {
126    type Error = LcelError;
127
128    /// Execute all steps in parallel using tokio tasks.
129    async fn invoke(
130        &self,
131        input: I,
132        config: Option<RunnableConfig>,
133    ) -> Result<HashMap<String, Value>, LcelError> {
134        let mut handles = Vec::with_capacity(self.steps.len());
135
136        for (key, step) in &self.steps {
137            let key = key.clone();
138            let step = step.clone();
139            let input = input.clone();
140            let config = config.clone();
141
142            let handle = tokio::spawn(async move {
143                let value = step.invoke(input, config).await?;
144                Ok::<(String, Value), LcelError>((key, value))
145            });
146
147            handles.push(handle);
148        }
149
150        let mut results = HashMap::new();
151        for handle in handles {
152            let (k, v) = handle
153                .await
154                .map_err(|e| LcelError::Other(format!("parallel task join error: {}", e)))?
155                ?;
156            results.insert(k, v);
157        }
158
159        Ok(results)
160    }
161
162    /// Batch: each step processes all inputs independently.
163    async fn batch(
164        &self,
165        inputs: Vec<I>,
166        config: Option<RunnableConfig>,
167    ) -> Result<Vec<HashMap<String, Value>>, LcelError> {
168        let mut results = Vec::with_capacity(inputs.len());
169        for input in inputs {
170            results.push(self.invoke(input, config.clone()).await?);
171        }
172        Ok(results)
173    }
174
175    /// Stream: invoke and return single-element stream.
176    async fn stream(
177        &self,
178        input: I,
179        config: Option<RunnableConfig>,
180    ) -> Result<Pin<Box<dyn Stream<Item = Result<HashMap<String, Value>, LcelError>> + Send>>, LcelError> {
181        let result = self.invoke(input, config).await?;
182        Ok(Box::pin(futures_util::stream::once(async move { Ok(result) })))
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::RunnableLambda;
190    use futures_util::StreamExt;
191
192    #[tokio::test]
193    async fn parallel_invoke() {
194        let parallel = RunnableParallel::<String>::new()
195            .with("len", RunnableLambda::new_sync(|s: String| s.len() as i64))
196            .with("upper", RunnableLambda::new_sync(|s: String| s.to_uppercase()));
197
198        let result = parallel.invoke("hello".to_string(), None).await.unwrap();
199        assert_eq!(
200            result.get("len").unwrap(),
201            &Value::Number(serde_json::Number::from(5))
202        );
203        assert_eq!(
204            result.get("upper").unwrap(),
205            &Value::String("HELLO".to_string())
206        );
207    }
208
209    #[tokio::test]
210    async fn parallel_empty() {
211        let parallel = RunnableParallel::<i32>::new();
212        let result = parallel.invoke(42, None).await.unwrap();
213        assert!(result.is_empty());
214    }
215
216    #[tokio::test]
217    async fn parallel_batch() {
218        let parallel = RunnableParallel::<String>::new()
219            .with("len", RunnableLambda::new_sync(|s: String| s.len() as i64));
220
221        let results = parallel
222            .batch(vec!["hi".to_string(), "hello".to_string()], None)
223            .await
224            .unwrap();
225        assert_eq!(results.len(), 2);
226        assert_eq!(
227            results[0].get("len").unwrap(),
228            &Value::Number(serde_json::Number::from(2))
229        );
230        assert_eq!(
231            results[1].get("len").unwrap(),
232            &Value::Number(serde_json::Number::from(5))
233        );
234    }
235}