lc_core/runnables/
parallel.rs1use 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
18pub 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 pub fn new() -> Self {
57 Self { steps: Vec::new() }
58 }
59
60 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 pub fn len(&self) -> usize {
83 self.steps.len()
84 }
85
86 pub fn is_empty(&self) -> bool {
88 self.steps.is_empty()
89 }
90}
91
92#[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
98struct 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 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 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 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}