1use async_trait::async_trait;
7use lc_core::runnables::RunnableConfig;
8use serde_json::Value;
9use std::collections::HashMap;
10use std::sync::Arc;
11
12use crate::base::{
13 run_chain_with_callbacks, stream_chain_with_callbacks, BaseChain, ChainError, ChainResult,
14 ChainStream,
15};
16
17pub struct SequentialChain {
22 chains: Vec<ChainStep>,
24
25 name: String,
27}
28
29struct ChainStep {
31 chain: Arc<dyn BaseChain>,
33
34 input_mapping: HashMap<String, String>,
36
37 output_mapping: HashMap<String, String>,
39}
40
41impl SequentialChain {
42 pub fn new() -> Self {
44 Self {
45 chains: Vec::new(),
46 name: "sequential_chain".to_string(),
47 }
48 }
49
50 pub fn with_name(mut self, name: impl Into<String>) -> Self {
52 self.name = name.into();
53 self
54 }
55
56 pub fn add_chain(
63 mut self,
64 chain: Arc<dyn BaseChain>,
65 input_keys: Vec<&str>,
66 output_keys: Vec<&str>,
67 ) -> Self {
68 let input_mapping = input_keys
69 .into_iter()
70 .map(|k| (k.to_string(), k.to_string()))
71 .collect();
72
73 let output_mapping = output_keys
74 .into_iter()
75 .map(|k| (k.to_string(), k.to_string()))
76 .collect();
77
78 self.chains.push(ChainStep {
79 chain,
80 input_mapping,
81 output_mapping,
82 });
83
84 self
85 }
86
87 pub fn add_chain_with_mapping(
94 mut self,
95 chain: Arc<dyn BaseChain>,
96 input_mapping: HashMap<String, String>,
97 output_mapping: HashMap<String, String>,
98 ) -> Self {
99 self.chains.push(ChainStep {
100 chain,
101 input_mapping,
102 output_mapping,
103 });
104
105 self
106 }
107
108 async fn run_steps(
112 &self,
113 inputs: HashMap<String, Value>,
114 config: Option<RunnableConfig>,
115 ) -> Result<ChainResult, ChainError> {
116 let mut current_state = inputs.clone();
117 let mut final_output = HashMap::new();
118
119 for (step_index, step) in self.chains.iter().enumerate() {
120 let mut chain_inputs = HashMap::new();
121 for (chain_key, global_key) in &step.input_mapping {
122 if let Some(value) = current_state.get(global_key) {
123 chain_inputs.insert(chain_key.clone(), value.clone());
124 } else {
125 return Err(ChainError::MissingInput(format!(
126 "Step {}: missing input '{}' (mapped from '{}')",
127 step_index, chain_key, global_key
128 )));
129 }
130 }
131
132 let chain_output = step
133 .chain
134 .invoke_with_config(chain_inputs, config.clone())
135 .await
136 .map_err(|e| ChainError::Nested {
137 context: format!(
138 "Step {} ({}) execution failed",
139 step_index,
140 step.chain.name()
141 ),
142 source: Box::new(e),
143 })?;
144
145 for (chain_key, global_key) in &step.output_mapping {
146 if let Some(value) = chain_output.get(chain_key) {
147 current_state.insert(global_key.clone(), value.clone());
148 final_output.insert(global_key.clone(), value.clone());
149 } else {
150 return Err(ChainError::OutputError(format!(
151 "Step {} ({}) did not produce expected output key '{}' (mapped to '{}')",
152 step_index,
153 step.chain.name(),
154 chain_key,
155 global_key
156 )));
157 }
158 }
159 }
160
161 Ok(final_output)
162 }
163
164 async fn stream_steps(
168 &self,
169 inputs: HashMap<String, Value>,
170 config: Option<RunnableConfig>,
171 ) -> Result<ChainStream, ChainError> {
172 if self.chains.is_empty() {
173 return Err(ChainError::ExecutionError(
174 "SequentialChain has no chains".to_string(),
175 ));
176 }
177
178 let mut current_state = inputs.clone();
179
180 let last_idx = self.chains.len() - 1;
182 for (step_index, step) in self.chains[..last_idx].iter().enumerate() {
183 let mut chain_inputs = HashMap::new();
184 for (chain_key, global_key) in &step.input_mapping {
185 if let Some(value) = current_state.get(global_key) {
186 chain_inputs.insert(chain_key.clone(), value.clone());
187 } else {
188 return Err(ChainError::MissingInput(format!(
189 "Step {}: missing input '{}' (mapped from '{}')",
190 step_index, chain_key, global_key
191 )));
192 }
193 }
194
195 let chain_output = step
196 .chain
197 .invoke_with_config(chain_inputs, config.clone())
198 .await
199 .map_err(|e| ChainError::Nested {
200 context: format!(
201 "Step {} ({}) execution failed",
202 step_index,
203 step.chain.name()
204 ),
205 source: Box::new(e),
206 })?;
207
208 for (chain_key, global_key) in &step.output_mapping {
209 if let Some(value) = chain_output.get(chain_key) {
210 current_state.insert(global_key.clone(), value.clone());
211 } else {
212 return Err(ChainError::OutputError(format!(
213 "Step {} ({}) did not produce expected output key '{}'",
214 step_index,
215 step.chain.name(),
216 chain_key,
217 )));
218 }
219 }
220 }
221
222 let last_step = &self.chains[last_idx];
224 let mut chain_inputs = HashMap::new();
225 for (chain_key, global_key) in &last_step.input_mapping {
226 if let Some(value) = current_state.get(global_key) {
227 chain_inputs.insert(chain_key.clone(), value.clone());
228 } else {
229 return Err(ChainError::MissingInput(format!(
230 "Last step: missing input '{}' (mapped from '{}')",
231 chain_key, global_key
232 )));
233 }
234 }
235
236 for (chain_key, global_key) in &last_step.output_mapping {
246 if !last_step.chain.output_keys().contains(&chain_key.as_str()) {
247 return Err(ChainError::OutputError(format!(
248 "Last step ({}) did not produce expected output key '{}' (mapped to '{}')",
249 last_step.chain.name(),
250 chain_key,
251 global_key
252 )));
253 }
254 }
255
256 last_step
257 .chain
258 .stream_with_config(chain_inputs, config.clone())
259 .await
260 }
261}
262
263impl Default for SequentialChain {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269#[async_trait]
270impl BaseChain for SequentialChain {
271 fn input_keys(&self) -> Vec<&str> {
272 if let Some(first) = self.chains.first() {
273 first.input_mapping.values().map(|s| s.as_str()).collect()
274 } else {
275 vec![]
276 }
277 }
278
279 fn output_keys(&self) -> Vec<&str> {
280 if let Some(last) = self.chains.last() {
281 last.output_mapping.values().map(|s| s.as_str()).collect()
282 } else {
283 vec![]
284 }
285 }
286
287 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
288 self.run_steps(inputs, None).await
289 }
290
291 async fn invoke_with_config(
298 &self,
299 inputs: HashMap<String, Value>,
300 config: Option<RunnableConfig>,
301 ) -> Result<ChainResult, ChainError> {
302 run_chain_with_callbacks(self.name(), inputs, config.clone(), |inputs| async move {
303 self.run_steps(inputs, config).await
304 })
305 .await
306 }
307
308 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
316 self.stream_steps(inputs, None).await
317 }
318
319 async fn stream_with_config(
325 &self,
326 inputs: HashMap<String, Value>,
327 config: Option<RunnableConfig>,
328 ) -> Result<ChainStream, ChainError> {
329 let output_key = self.output_keys().first().map(|k| (*k).to_string());
330 stream_chain_with_callbacks(
331 self.name(),
332 inputs,
333 config.clone(),
334 output_key,
335 |inputs| async move { self.stream_steps(inputs, config).await },
336 )
337 .await
338 }
339
340 fn name(&self) -> &str {
341 &self.name
342 }
343}
344
345impl std::fmt::Debug for SequentialChain {
346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347 f.debug_struct("SequentialChain")
348 .field("steps", &self.chains.len())
349 .field("name", &self.name)
350 .finish()
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use async_trait::async_trait;
358 use futures_util::StreamExt;
359 use serde_json::json;
360
361 struct EchoChain {
363 input_key: String,
364 output_key: String,
365 }
366
367 impl EchoChain {
368 fn new(input_key: &str, output_key: &str) -> Self {
369 Self {
370 input_key: input_key.to_string(),
371 output_key: output_key.to_string(),
372 }
373 }
374 }
375
376 #[async_trait]
377 impl BaseChain for EchoChain {
378 fn input_keys(&self) -> Vec<&str> {
379 vec![&self.input_key]
380 }
381 fn output_keys(&self) -> Vec<&str> {
382 vec![&self.output_key]
383 }
384 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
385 let mut result = HashMap::new();
386 if let Some(v) = inputs.get(&self.input_key) {
387 result.insert(self.output_key.clone(), v.clone());
388 }
389 Ok(result)
390 }
391 }
392
393 struct FailingChain;
395
396 #[async_trait]
397 impl BaseChain for FailingChain {
398 fn input_keys(&self) -> Vec<&str> {
399 vec!["text"]
400 }
401 fn output_keys(&self) -> Vec<&str> {
402 vec![]
403 }
404 async fn invoke(&self, _inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
405 Err(ChainError::MissingInput("nested_key".to_string()))
406 }
407 }
408
409 struct UppercaseChain {
411 input_key: String,
412 output_key: String,
413 }
414
415 #[async_trait]
416 impl BaseChain for UppercaseChain {
417 fn input_keys(&self) -> Vec<&str> {
418 vec![&self.input_key]
419 }
420 fn output_keys(&self) -> Vec<&str> {
421 vec![&self.output_key]
422 }
423 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
424 let mut result = HashMap::new();
425 if let Some(Value::String(s)) = inputs.get(&self.input_key) {
426 result.insert(self.output_key.clone(), Value::String(s.to_uppercase()));
427 }
428 Ok(result)
429 }
430 }
431
432 #[tokio::test]
433 async fn test_sequential_chain_single_step() {
434 let chain = SequentialChain::new().add_chain(
435 Arc::new(EchoChain::new("text", "result")),
436 vec!["text"],
437 vec!["result"],
438 );
439
440 let mut inputs = HashMap::new();
441 inputs.insert("text".to_string(), json!("hello"));
442
443 let result = chain.invoke(inputs).await.unwrap();
444 assert_eq!(result.get("result").unwrap(), &json!("hello"));
445 }
446
447 #[tokio::test]
448 async fn test_sequential_chain_two_steps() {
449 let chain = SequentialChain::new()
450 .add_chain(
451 Arc::new(EchoChain::new("text", "intermediate")),
452 vec!["text"],
453 vec!["intermediate"],
454 )
455 .add_chain(
456 Arc::new(UppercaseChain {
457 input_key: "intermediate".to_string(),
458 output_key: "result".to_string(),
459 }),
460 vec!["intermediate"],
461 vec!["result"],
462 );
463
464 let mut inputs = HashMap::new();
465 inputs.insert("text".to_string(), json!("hello"));
466
467 let result = chain.invoke(inputs).await.unwrap();
468 assert_eq!(result.get("result").unwrap(), &json!("HELLO"));
469 }
470
471 #[tokio::test]
472 async fn test_sequential_chain_missing_input() {
473 let chain = SequentialChain::new().add_chain(
474 Arc::new(EchoChain::new("text", "result")),
475 vec!["text"],
476 vec!["result"],
477 );
478
479 let inputs = HashMap::new();
480 let result = chain.invoke(inputs).await;
481 assert!(result.is_err());
482 }
483
484 #[tokio::test]
488 async fn test_sequential_chain_preserves_subchain_error() {
489 let chain = SequentialChain::new().add_chain(Arc::new(FailingChain), vec!["text"], vec![]);
490 let mut inputs = HashMap::new();
491 inputs.insert("text".to_string(), json!("hello"));
492 let err = chain.invoke(inputs).await.unwrap_err();
493 match &err {
494 ChainError::Nested { context, source } => {
495 assert!(context.contains("Step 0"), "context: {context}");
496 let downcast = source.downcast_ref::<ChainError>();
497 assert!(
498 matches!(downcast, Some(ChainError::MissingInput(k)) if k == "nested_key"),
499 "source should downcast to the original variant, got {downcast:?}"
500 );
501 }
502 other => panic!("expected ChainError::Nested, got {other:?}"),
503 }
504 }
505
506 #[tokio::test]
511 async fn test_sequential_chain_stream_applies_output_mapping() {
512 let chain = SequentialChain::new()
513 .add_chain_with_mapping(
514 Arc::new(EchoChain::new("text", "intermediate")),
515 HashMap::from([("text".to_string(), "text".to_string())]),
516 HashMap::from([("intermediate".to_string(), "intermediate".to_string())]),
517 )
518 .add_chain_with_mapping(
519 Arc::new(UppercaseChain {
520 input_key: "intermediate".to_string(),
521 output_key: "answer".to_string(),
522 }),
523 HashMap::from([("intermediate".to_string(), "intermediate".to_string())]),
524 HashMap::from([("answer".to_string(), "final_result".to_string())]),
525 );
526
527 assert_eq!(chain.output_keys(), vec!["final_result"]);
529
530 let mut inputs = HashMap::new();
531 inputs.insert("text".to_string(), json!("hello"));
532
533 let mut stream = chain.stream(inputs).await.unwrap();
534 let mut tokens = Vec::new();
535 while let Some(item) = stream.next().await {
536 tokens.push(item.unwrap());
537 }
538 let text: String = tokens.iter().map(|t| t.token.as_str()).collect();
539 assert_eq!(text, "HELLO");
540 assert!(tokens.last().unwrap().is_final);
541 }
542
543 #[tokio::test]
549 async fn test_sequential_chain_stream_validates_output_mapping() {
550 let chain = SequentialChain::new().add_chain_with_mapping(
551 Arc::new(EchoChain::new("text", "result")),
552 HashMap::from([("text".to_string(), "text".to_string())]),
553 HashMap::from([("missing".to_string(), "final_result".to_string())]),
554 );
555
556 let mut inputs = HashMap::new();
557 inputs.insert("text".to_string(), json!("hello"));
558
559 let err = match chain.stream(inputs).await {
560 Ok(_) => panic!("expected an OutputError"),
561 Err(e) => e,
562 };
563 assert!(
564 matches!(err, ChainError::OutputError(_)),
565 "expected OutputError, got {err:?}"
566 );
567 }
568
569 #[tokio::test]
570 async fn test_sequential_chain_with_name() {
571 let chain = SequentialChain::new().with_name("my_chain");
572 assert_eq!(chain.name(), "my_chain");
573 }
574
575 #[tokio::test]
576 async fn test_sequential_chain_default() {
577 let chain = SequentialChain::default();
578 assert_eq!(chain.name(), "sequential_chain");
579 }
580
581 #[tokio::test]
582 async fn test_sequential_chain_debug() {
583 let chain = SequentialChain::new().with_name("test_chain");
584 let debug_str = format!("{:?}", chain);
585 assert!(debug_str.contains("test_chain"));
586 assert!(debug_str.contains("0")); }
588
589 #[tokio::test]
590 async fn test_sequential_chain_input_keys() {
591 let chain = SequentialChain::new().add_chain(
592 Arc::new(EchoChain::new("query", "result")),
593 vec!["query"],
594 vec!["result"],
595 );
596 assert_eq!(chain.input_keys(), vec!["query"]);
597 }
598
599 #[tokio::test]
600 async fn test_sequential_chain_output_keys() {
601 let chain = SequentialChain::new().add_chain(
602 Arc::new(EchoChain::new("query", "answer")),
603 vec!["query"],
604 vec!["answer"],
605 );
606 assert_eq!(chain.output_keys(), vec!["answer"]);
607 }
608}