1use async_trait::async_trait;
5use futures_util::{stream, Stream, StreamExt};
6use lc_callbacks::{CallbackManager, RunTree, RunType};
7use lc_core::runnables::RunnableConfig;
8use lc_schema::Message;
9use lc_shared::document::Document;
10use serde_json::{json, Value};
11use std::collections::HashMap;
12use std::future::Future;
13use std::pin::Pin;
14use std::sync::Arc;
15
16#[derive(Debug, thiserror::Error)]
18#[non_exhaustive]
19pub enum ChainError {
20 #[error("Missing input: {0}")]
22 MissingInput(String),
23
24 #[error("Input error: {0}")]
26 InputError(String),
27
28 #[error("Output error: {0}")]
30 OutputError(String),
31
32 #[error("Execution error: {0}")]
34 ExecutionError(String),
35
36 #[error("Stream error: {0}")]
38 StreamError(String),
39
40 #[error("Chain error: {0}")]
42 Other(String),
43
44 #[error("{context}: {source}")]
52 Nested {
53 context: String,
55 #[source]
57 source: Box<dyn std::error::Error + Send + Sync>,
58 },
59}
60
61pub type ChainResult = HashMap<String, Value>;
63
64#[derive(Debug, Clone)]
66pub struct StreamToken {
67 pub token: String,
69 pub is_final: bool,
71}
72
73pub type ChainStream = Pin<Box<dyn Stream<Item = Result<StreamToken, ChainError>> + Send>>;
75
76pub(crate) fn variables_to_messages(vars: &HashMap<String, Value>) -> Vec<Message> {
86 lc_memory::memory_variables_to_messages(vars)
89}
90
91pub(crate) fn documents_from_input(value: Option<&Value>) -> Result<Vec<Document>, ChainError> {
99 let arr = value
100 .and_then(|v| v.as_array())
101 .ok_or_else(|| ChainError::MissingInput("documents".to_string()))?;
102
103 let mut docs = Vec::with_capacity(arr.len());
104 let mut failed = 0usize;
105 for item in arr {
106 match serde_json::from_value::<Document>(item.clone()) {
107 Ok(doc) => docs.push(doc),
108 Err(_) => failed += 1,
109 }
110 }
111 if failed > 0 {
112 return Err(ChainError::InputError(format!(
113 "document deserialization failed: {failed} of {} document(s) lost",
114 arr.len()
115 )));
116 }
117 Ok(docs)
118}
119
120pub(crate) fn documents_to_values(documents: &[Document]) -> Result<Vec<Value>, ChainError> {
123 documents
124 .iter()
125 .map(|doc| {
126 serde_json::to_value(doc)
127 .map_err(|e| ChainError::Other(format!("failed to serialize document: {e}")))
128 })
129 .collect()
130}
131
132pub(crate) fn substitute_template(
151 template: &str,
152 vars: &HashMap<String, String>,
153) -> (String, Vec<String>) {
154 let chars: Vec<char> = template.chars().collect();
155 let n = chars.len();
156 let mut out = String::with_capacity(template.len());
157 let mut missing: Vec<String> = Vec::new();
158 let mut i = 0;
159
160 while i < n {
161 let c = chars[i];
162
163 if c == '{' {
164 if i + 1 < n && chars[i + 1] == '{' {
166 out.push('{');
167 i += 2;
168 continue;
169 }
170
171 let mut j = i + 1;
173 while j < n && chars[j] != '}' {
174 j += 1;
175 }
176 if j < n {
177 let name: String = chars[i + 1..j].iter().collect();
178 if is_valid_template_var_name(&name) {
179 match vars.get(&name) {
180 Some(v) => out.push_str(v),
181 None => {
182 if !missing.contains(&name) {
183 missing.push(name.clone());
184 }
185 out.push('{');
187 out.push_str(&name);
188 out.push('}');
189 }
190 }
191 i = j + 1;
192 continue;
193 }
194 }
195
196 out.push('{');
198 i += 1;
199 continue;
200 }
201
202 if c == '}' {
203 if i + 1 < n && chars[i + 1] == '}' {
205 out.push('}');
206 i += 2;
207 continue;
208 }
209 out.push('}');
211 i += 1;
212 continue;
213 }
214
215 out.push(c);
216 i += 1;
217 }
218
219 (out, missing)
220}
221
222fn is_valid_template_var_name(name: &str) -> bool {
226 let mut chars = name.chars();
227 match chars.next() {
228 Some(c) if c.is_alphabetic() || c == '_' => {}
229 _ => return false,
230 }
231 chars.all(|c| c.is_alphanumeric() || c == '_')
232}
233
234#[async_trait]
238pub trait BaseChain: Send + Sync {
239 fn input_keys(&self) -> Vec<&str>;
241
242 fn output_keys(&self) -> Vec<&str>;
244
245 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError>;
253
254 async fn invoke_with_config(
264 &self,
265 inputs: HashMap<String, Value>,
266 config: Option<RunnableConfig>,
267 ) -> Result<ChainResult, ChainError> {
268 run_chain_with_callbacks(self.name(), inputs, config, |inputs| async move {
269 self.invoke(inputs).await
270 })
271 .await
272 }
273
274 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
286 let result = self.invoke(inputs).await?;
288 let as_str = |v: &Value| v.as_str().map(|s| s.to_string());
296 let output_text = self
297 .output_keys()
298 .iter()
299 .find_map(|k| result.get(*k).and_then(as_str))
300 .or_else(|| {
301 if result.len() == 1 {
302 result.values().next().and_then(as_str)
303 } else {
304 result
305 .keys()
306 .min()
307 .and_then(|k| result.get(k).and_then(as_str))
308 }
309 })
310 .ok_or_else(|| {
311 ChainError::OutputError("chain produced no string output to stream".to_string())
312 })?;
313 let stream = futures_util::stream::once(async move {
314 Ok(StreamToken {
315 token: output_text,
316 is_final: true,
317 })
318 });
319 Ok(Box::pin(stream))
320 }
321
322 async fn stream_with_config(
328 &self,
329 inputs: HashMap<String, Value>,
330 config: Option<RunnableConfig>,
331 ) -> Result<ChainStream, ChainError> {
332 let output_key = self.output_keys().first().map(|k| (*k).to_string());
333 stream_chain_with_callbacks(
334 self.name(),
335 inputs,
336 config,
337 output_key,
338 |inputs| async move {
339 self.stream(inputs).await
340 },
341 )
342 .await
343 }
344
345 fn validate_inputs(&self, inputs: &HashMap<String, Value>) -> Result<(), ChainError> {
347 for key in self.input_keys() {
348 if !inputs.contains_key(key) {
349 return Err(ChainError::MissingInput(key.to_string()));
350 }
351 }
352 Ok(())
353 }
354
355 fn name(&self) -> &str {
357 "chain"
358 }
359}
360
361pub(crate) async fn run_chain_with_callbacks<F, Fut>(
369 name: &str,
370 inputs: HashMap<String, Value>,
371 config: Option<RunnableConfig>,
372 body: F,
373) -> Result<ChainResult, ChainError>
374where
375 F: FnOnce(HashMap<String, Value>) -> Fut,
376 Fut: Future<Output = Result<ChainResult, ChainError>> + Send,
377{
378 let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
379 let mut run = RunTree::new(name, RunType::Chain, json!({ "inputs": inputs }));
380
381 if let Some(ref cb) = callbacks {
382 cb.dispatch_chain_start(&run, &run.inputs).await;
383 }
384
385 let result = body(inputs).await;
386
387 match result {
388 Ok(output) => {
389 run.end(json!({ "output": output }));
390 if let Some(ref cb) = callbacks {
391 cb.dispatch_chain_end(&run, &json!({ "output": output }))
392 .await;
393 }
394 Ok(output)
395 }
396 Err(e) => {
397 let msg = e.to_string();
398 run.end_with_error(msg.clone());
399 if let Some(ref cb) = callbacks {
400 cb.dispatch_chain_error(&run, &msg).await;
401 }
402 Err(e)
403 }
404 }
405}
406
407pub(crate) async fn stream_chain_with_callbacks<F, Fut>(
418 name: &str,
419 inputs: HashMap<String, Value>,
420 config: Option<RunnableConfig>,
421 output_key: Option<String>,
422 body: F,
423) -> Result<ChainStream, ChainError>
424where
425 F: FnOnce(HashMap<String, Value>) -> Fut,
426 Fut: Future<Output = Result<ChainStream, ChainError>> + Send,
427{
428 let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
429 let mut run = RunTree::new(name, RunType::Chain, json!({ "inputs": inputs }));
430
431 if let Some(ref cb) = callbacks {
432 cb.dispatch_chain_start(&run, &run.inputs).await;
433 }
434
435 let stream = match body(inputs).await {
436 Ok(s) => s,
437 Err(e) => {
438 let msg = e.to_string();
439 run.end_with_error(msg.clone());
440 if let Some(ref cb) = callbacks {
441 cb.dispatch_chain_error(&run, &msg).await;
442 }
443 return Err(e);
444 }
445 };
446
447 Ok(Box::pin(end_stream_on_completion(
448 stream,
449 run,
450 callbacks,
451 output_key,
452 )))
453}
454
455fn end_stream_on_completion(
461 inner: ChainStream,
462 run: RunTree,
463 callbacks: Option<Arc<CallbackManager>>,
464 output_key: Option<String>,
465) -> impl Stream<Item = Result<StreamToken, ChainError>> + Send {
466 stream::unfold(
467 Some((inner, run, callbacks, output_key, String::new())),
468 |state| async move {
469 let (mut inner, run, callbacks, output_key, mut accumulated) = match state {
470 Some(s) => s,
471 None => return None,
472 };
473 match inner.next().await {
474 Some(Ok(token)) => {
475 accumulated.push_str(&token.token);
476 Some((
477 Ok(token),
478 Some((inner, run, callbacks, output_key, accumulated)),
479 ))
480 }
481 Some(Err(e)) => {
482 let msg = e.to_string();
483 let mut run = run;
484 run.end_with_error(msg.clone());
485 if let Some(cb) = callbacks {
486 cb.dispatch_chain_error(&run, &msg).await;
487 }
488 Some((Err(e), None))
489 }
490 None => {
491 let mut run = run;
492 let key = output_key.unwrap_or_else(|| "output".to_string());
495 let payload = json!({ key: accumulated });
496 run.end(json!({ "output": payload }));
497 if let Some(cb) = callbacks {
498 cb.dispatch_chain_end(&run, &json!({ "output": payload }))
499 .await;
500 }
501 None
502 }
503 }
504 },
505 )
506}
507
508#[cfg(test)]
509mod tests {
510 use super::*;
511 use std::error::Error;
512
513 #[test]
516 fn test_substitute_template_no_value_rescan() {
517 let mut vars = HashMap::new();
518 vars.insert("question".to_string(), "value with {summaries} inside".to_string());
519 vars.insert("summaries".to_string(), "SHOULD_NOT_APPEAR".to_string());
520 let (out, missing) = substitute_template("Q: {question} S: {summaries}", &vars);
521 assert_eq!(
522 out,
523 "Q: value with {summaries} inside S: SHOULD_NOT_APPEAR"
524 );
525 assert!(missing.is_empty());
526 }
527
528 #[test]
531 fn test_substitute_template_cjk_and_missing() {
532 let mut vars = HashMap::new();
533 vars.insert("姓名".to_string(), "张三".to_string());
534 let (out, missing) = substitute_template("你好,{姓名}!{缺失}", &vars);
535 assert_eq!(out, "你好,张三!{缺失}");
536 assert_eq!(missing, vec!["缺失".to_string()]);
537 }
538
539 #[test]
541 fn test_substitute_template_escaped_braces() {
542 let mut vars = HashMap::new();
543 vars.insert("x".to_string(), "V".to_string());
544 let (out, missing) = substitute_template("{{literal}} {x} }}end{{", &vars);
545 assert_eq!(out, "{literal} V }end{");
546 assert!(missing.is_empty());
547 }
548
549 #[test]
550 fn test_chain_error_display() {
551 let error = ChainError::MissingInput("test".to_string());
552 assert!(error.to_string().contains("Missing input"));
553
554 let error = ChainError::ExecutionError("test".to_string());
555 assert!(error.to_string().contains("Execution error"));
556 }
557
558 #[test]
559 fn test_chain_error_all_variants() {
560 let err = ChainError::MissingInput("key".to_string());
561 assert!(err.to_string().contains("key"));
562
563 let err = ChainError::OutputError("bad".to_string());
564 assert!(err.to_string().contains("bad"));
565
566 let err = ChainError::ExecutionError("fail".to_string());
567 assert!(err.to_string().contains("fail"));
568
569 let err = ChainError::StreamError("broken".to_string());
570 assert!(err.to_string().contains("broken"));
571
572 let err = ChainError::Other("misc".to_string());
573 assert!(err.to_string().contains("misc"));
574 }
575
576 #[test]
580 fn test_chain_error_nested_preserves_source() {
581 let inner = ChainError::MissingInput("text".to_string());
582 let nested = ChainError::Nested {
583 context: "Step 0 (echo) execution failed".to_string(),
584 source: Box::new(inner),
585 };
586 assert!(nested
587 .to_string()
588 .contains("Step 0 (echo) execution failed"));
589 assert!(nested.to_string().contains("Missing input"));
590
591 let source = nested.source().expect("Nested must carry a source");
592 let downcast = source.downcast_ref::<ChainError>();
593 assert!(
594 matches!(downcast, Some(ChainError::MissingInput(k)) if k == "text"),
595 "source should downcast back to the original variant, got {downcast:?}"
596 );
597 }
598
599 #[test]
600 fn test_stream_token_debug() {
601 let token = StreamToken {
602 token: "hello".to_string(),
603 is_final: false,
604 };
605 assert!(format!("{:?}", token).contains("hello"));
606 }
607
608 #[tokio::test]
612 async fn test_default_stream_errors_on_non_string_output() {
613 struct NonStringChain;
614 #[async_trait]
615 impl BaseChain for NonStringChain {
616 fn input_keys(&self) -> Vec<&str> {
617 vec![]
618 }
619 fn output_keys(&self) -> Vec<&str> {
620 vec!["count"]
621 }
622 async fn invoke(
623 &self,
624 _inputs: HashMap<String, Value>,
625 ) -> Result<ChainResult, ChainError> {
626 let mut result = HashMap::new();
627 result.insert("count".to_string(), json!(3));
628 Ok(result)
629 }
630 }
631
632 let chain = NonStringChain;
633 let err = match chain.stream(HashMap::new()).await {
634 Ok(_) => panic!("expected an OutputError"),
635 Err(e) => e,
636 };
637 assert!(
638 matches!(err, ChainError::OutputError(_)),
639 "expected OutputError, got {err:?}"
640 );
641 }
642
643 #[test]
644 fn test_validate_inputs_pass() {
645 struct PassthroughChain;
646 #[async_trait]
647 impl BaseChain for PassthroughChain {
648 fn input_keys(&self) -> Vec<&str> {
649 vec!["input"]
650 }
651 fn output_keys(&self) -> Vec<&str> {
652 vec!["output"]
653 }
654 async fn invoke(
655 &self,
656 inputs: HashMap<String, Value>,
657 ) -> Result<ChainResult, ChainError> {
658 Ok(inputs)
659 }
660 }
661
662 let chain = PassthroughChain;
663 let mut inputs = HashMap::new();
664 inputs.insert("input".to_string(), Value::String("test".to_string()));
665 assert!(chain.validate_inputs(&inputs).is_ok());
666 }
667
668 #[test]
669 fn test_validate_inputs_missing_key() {
670 struct PassthroughChain;
671 #[async_trait]
672 impl BaseChain for PassthroughChain {
673 fn input_keys(&self) -> Vec<&str> {
674 vec!["input"]
675 }
676 fn output_keys(&self) -> Vec<&str> {
677 vec!["output"]
678 }
679 async fn invoke(
680 &self,
681 _inputs: HashMap<String, Value>,
682 ) -> Result<ChainResult, ChainError> {
683 Ok(HashMap::new())
684 }
685 }
686
687 let chain = PassthroughChain;
688 let inputs = HashMap::new();
689 assert!(chain.validate_inputs(&inputs).is_err());
690 }
691
692 #[test]
693 fn test_default_chain_name() {
694 struct MyChain;
695 #[async_trait]
696 impl BaseChain for MyChain {
697 fn input_keys(&self) -> Vec<&str> {
698 vec![]
699 }
700 fn output_keys(&self) -> Vec<&str> {
701 vec![]
702 }
703 async fn invoke(
704 &self,
705 _inputs: HashMap<String, Value>,
706 ) -> Result<ChainResult, ChainError> {
707 Ok(HashMap::new())
708 }
709 }
710 let chain = MyChain;
711 assert_eq!(chain.name(), "chain");
712 }
713}