rune_chain_parallel/
lib.rs1use std::collections::HashMap;
46use std::sync::Arc;
47
48use async_trait::async_trait;
49use futures::future::join_all;
50use rune_chain_core::{Chain, ChainError, GenerateResult, PromptArgs, TokenUsage};
51use serde_json::{Value, json};
52
53struct Branch {
54 chain: Arc<dyn Chain>,
55 output_key: String,
56}
57
58pub struct ParallelChain {
63 branches: Vec<Branch>,
64}
65
66impl ParallelChain {
67 pub fn new() -> Self {
69 Self {
70 branches: Vec::new(),
71 }
72 }
73
74 pub fn branch(mut self, chain: impl Chain + 'static, output_key: impl Into<String>) -> Self {
95 self.branches.push(Branch {
96 chain: Arc::new(chain),
97 output_key: output_key.into(),
98 });
99 self
100 }
101}
102
103impl Default for ParallelChain {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109#[async_trait]
110impl Chain for ParallelChain {
111 async fn call(&self, input: PromptArgs) -> Result<GenerateResult, ChainError> {
112 let map = self.execute(input).await?;
113 let result_value = map
114 .get("generate_result")
115 .cloned()
116 .unwrap_or(json!(GenerateResult::default()));
117 serde_json::from_value(result_value)
118 .map_err(|e| ChainError::Other(format!("result deserialisation failed: {e}")))
119 }
120
121 async fn execute(&self, input: PromptArgs) -> Result<HashMap<String, Value>, ChainError> {
122 let futures: Vec<_> = self
123 .branches
124 .iter()
125 .map(|b| {
126 let chain = Arc::clone(&b.chain);
127 let input_clone = input.clone();
128 let key = b.output_key.clone();
129 async move {
130 let result = chain.call(input_clone).await;
131 (key, result)
132 }
133 })
134 .collect();
135
136 let results = join_all(futures).await;
137
138 let mut accumulated: HashMap<String, Value> = HashMap::new();
139 let mut total_tokens: Option<TokenUsage> = None;
140 let mut last_generation = String::new();
141
142 for (key, result) in results {
143 let r = result?;
144 last_generation = r.generation.clone();
145 if let Some(usage) = &r.tokens {
146 total_tokens = Some(match total_tokens.take() {
147 None => usage.clone(),
148 Some(prev) => prev.combine(usage),
149 });
150 }
151 accumulated.insert(key, json!(r.generation));
152 }
153
154 let final_result = GenerateResult {
155 generation: last_generation,
156 tokens: total_tokens,
157 tool_calls: vec![],
158 };
159
160 accumulated.insert(
161 "generate_result".to_string(),
162 serde_json::to_value(&final_result).unwrap_or(Value::Null),
163 );
164
165 Ok(accumulated)
166 }
167
168 fn output_keys(&self) -> Vec<String> {
169 let mut keys: Vec<String> = self.branches.iter().map(|b| b.output_key.clone()).collect();
170 keys.push("generate_result".to_string());
171 keys
172 }
173
174 fn input_keys(&self) -> Vec<String> {
175 self.branches
176 .first()
177 .map(|b| b.chain.input_keys())
178 .unwrap_or_default()
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use async_trait::async_trait;
186 use rune_chain_core::prompt_args;
187
188 struct Echo(String);
189
190 #[async_trait]
191 impl Chain for Echo {
192 async fn call(&self, input: PromptArgs) -> Result<GenerateResult, ChainError> {
193 let text = input
194 .get(&self.0)
195 .and_then(|v| v.as_str())
196 .unwrap_or("")
197 .to_string();
198 Ok(GenerateResult::from_text(text))
199 }
200 }
201
202 struct Upper(String);
203
204 #[async_trait]
205 impl Chain for Upper {
206 async fn call(&self, input: PromptArgs) -> Result<GenerateResult, ChainError> {
207 let text = input
208 .get(&self.0)
209 .and_then(|v| v.as_str())
210 .unwrap_or("")
211 .to_uppercase();
212 Ok(GenerateResult::from_text(text))
213 }
214 }
215
216 #[tokio::test]
217 async fn runs_branches_and_merges_output() {
218 let par = ParallelChain::new()
219 .branch(Echo("input".into()), "echo")
220 .branch(Upper("input".into()), "upper");
221
222 let map = par
223 .execute(prompt_args! { "input" => "hello" })
224 .await
225 .unwrap();
226 assert_eq!(map["echo"].as_str().unwrap(), "hello");
227 assert_eq!(map["upper"].as_str().unwrap(), "HELLO");
228 }
229
230 #[tokio::test]
231 async fn output_keys_includes_all_branches() {
232 let par = ParallelChain::new()
233 .branch(Echo("input".into()), "a")
234 .branch(Echo("input".into()), "b");
235 assert_eq!(par.output_keys(), vec!["a", "b", "generate_result"]);
236 }
237
238 #[tokio::test]
239 async fn empty_parallel_chain_returns_default() {
240 let par = ParallelChain::new();
241 let result = par.call(prompt_args! {}).await.unwrap();
242 assert_eq!(result.generation, "");
243 }
244}