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 { self.stream(inputs).await },
339 )
340 .await
341 }
342
343 fn validate_inputs(&self, inputs: &HashMap<String, Value>) -> Result<(), ChainError> {
345 for key in self.input_keys() {
346 if !inputs.contains_key(key) {
347 return Err(ChainError::MissingInput(key.to_string()));
348 }
349 }
350 Ok(())
351 }
352
353 fn name(&self) -> &str {
355 "chain"
356 }
357}
358
359pub(crate) async fn run_chain_with_callbacks<F, Fut>(
367 name: &str,
368 inputs: HashMap<String, Value>,
369 config: Option<RunnableConfig>,
370 body: F,
371) -> Result<ChainResult, ChainError>
372where
373 F: FnOnce(HashMap<String, Value>) -> Fut,
374 Fut: Future<Output = Result<ChainResult, ChainError>> + Send,
375{
376 let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
377 let mut run = RunTree::new(name, RunType::Chain, json!({ "inputs": inputs }));
378
379 if let Some(ref cb) = callbacks {
380 cb.dispatch_chain_start(&run, &run.inputs).await;
381 }
382
383 let result = body(inputs).await;
384
385 match result {
386 Ok(output) => {
387 run.end(json!({ "output": output }));
388 if let Some(ref cb) = callbacks {
389 cb.dispatch_chain_end(&run, &json!({ "output": output }))
390 .await;
391 }
392 Ok(output)
393 }
394 Err(e) => {
395 let msg = e.to_string();
396 run.end_with_error(msg.clone());
397 if let Some(ref cb) = callbacks {
398 cb.dispatch_chain_error(&run, &msg).await;
399 }
400 Err(e)
401 }
402 }
403}
404
405pub(crate) async fn stream_chain_with_callbacks<F, Fut>(
416 name: &str,
417 inputs: HashMap<String, Value>,
418 config: Option<RunnableConfig>,
419 output_key: Option<String>,
420 body: F,
421) -> Result<ChainStream, ChainError>
422where
423 F: FnOnce(HashMap<String, Value>) -> Fut,
424 Fut: Future<Output = Result<ChainStream, ChainError>> + Send,
425{
426 let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
427 let mut run = RunTree::new(name, RunType::Chain, json!({ "inputs": inputs }));
428
429 if let Some(ref cb) = callbacks {
430 cb.dispatch_chain_start(&run, &run.inputs).await;
431 }
432
433 let stream = match body(inputs).await {
434 Ok(s) => s,
435 Err(e) => {
436 let msg = e.to_string();
437 run.end_with_error(msg.clone());
438 if let Some(ref cb) = callbacks {
439 cb.dispatch_chain_error(&run, &msg).await;
440 }
441 return Err(e);
442 }
443 };
444
445 Ok(Box::pin(end_stream_on_completion(
446 stream, run, callbacks, output_key,
447 )))
448}
449
450fn end_stream_on_completion(
456 inner: ChainStream,
457 run: RunTree,
458 callbacks: Option<Arc<CallbackManager>>,
459 output_key: Option<String>,
460) -> impl Stream<Item = Result<StreamToken, ChainError>> + Send {
461 stream::unfold(
462 Some((inner, run, callbacks, output_key, String::new())),
463 |state| async move {
464 let (mut inner, run, callbacks, output_key, mut accumulated) = match state {
465 Some(s) => s,
466 None => return None,
467 };
468 match inner.next().await {
469 Some(Ok(token)) => {
470 accumulated.push_str(&token.token);
471 Some((
472 Ok(token),
473 Some((inner, run, callbacks, output_key, accumulated)),
474 ))
475 }
476 Some(Err(e)) => {
477 let msg = e.to_string();
478 let mut run = run;
479 run.end_with_error(msg.clone());
480 if let Some(cb) = callbacks {
481 cb.dispatch_chain_error(&run, &msg).await;
482 }
483 Some((Err(e), None))
484 }
485 None => {
486 let mut run = run;
487 let key = output_key.unwrap_or_else(|| "output".to_string());
490 let payload = json!({ key: accumulated });
491 run.end(json!({ "output": payload }));
492 if let Some(cb) = callbacks {
493 cb.dispatch_chain_end(&run, &json!({ "output": payload }))
494 .await;
495 }
496 None
497 }
498 }
499 },
500 )
501}
502
503#[cfg(test)]
504mod tests {
505 use super::*;
506 use std::error::Error;
507
508 #[test]
511 fn test_substitute_template_no_value_rescan() {
512 let mut vars = HashMap::new();
513 vars.insert(
514 "question".to_string(),
515 "value with {summaries} inside".to_string(),
516 );
517 vars.insert("summaries".to_string(), "SHOULD_NOT_APPEAR".to_string());
518 let (out, missing) = substitute_template("Q: {question} S: {summaries}", &vars);
519 assert_eq!(out, "Q: value with {summaries} inside S: SHOULD_NOT_APPEAR");
520 assert!(missing.is_empty());
521 }
522
523 #[test]
526 fn test_substitute_template_cjk_and_missing() {
527 let mut vars = HashMap::new();
528 vars.insert("姓名".to_string(), "张三".to_string());
529 let (out, missing) = substitute_template("你好,{姓名}!{缺失}", &vars);
530 assert_eq!(out, "你好,张三!{缺失}");
531 assert_eq!(missing, vec!["缺失".to_string()]);
532 }
533
534 #[test]
536 fn test_substitute_template_escaped_braces() {
537 let mut vars = HashMap::new();
538 vars.insert("x".to_string(), "V".to_string());
539 let (out, missing) = substitute_template("{{literal}} {x} }}end{{", &vars);
540 assert_eq!(out, "{literal} V }end{");
541 assert!(missing.is_empty());
542 }
543
544 #[test]
545 fn test_chain_error_display() {
546 let error = ChainError::MissingInput("test".to_string());
547 assert!(error.to_string().contains("Missing input"));
548
549 let error = ChainError::ExecutionError("test".to_string());
550 assert!(error.to_string().contains("Execution error"));
551 }
552
553 #[test]
554 fn test_chain_error_all_variants() {
555 let err = ChainError::MissingInput("key".to_string());
556 assert!(err.to_string().contains("key"));
557
558 let err = ChainError::OutputError("bad".to_string());
559 assert!(err.to_string().contains("bad"));
560
561 let err = ChainError::ExecutionError("fail".to_string());
562 assert!(err.to_string().contains("fail"));
563
564 let err = ChainError::StreamError("broken".to_string());
565 assert!(err.to_string().contains("broken"));
566
567 let err = ChainError::Other("misc".to_string());
568 assert!(err.to_string().contains("misc"));
569 }
570
571 #[test]
575 fn test_chain_error_nested_preserves_source() {
576 let inner = ChainError::MissingInput("text".to_string());
577 let nested = ChainError::Nested {
578 context: "Step 0 (echo) execution failed".to_string(),
579 source: Box::new(inner),
580 };
581 assert!(nested
582 .to_string()
583 .contains("Step 0 (echo) execution failed"));
584 assert!(nested.to_string().contains("Missing input"));
585
586 let source = nested.source().expect("Nested must carry a source");
587 let downcast = source.downcast_ref::<ChainError>();
588 assert!(
589 matches!(downcast, Some(ChainError::MissingInput(k)) if k == "text"),
590 "source should downcast back to the original variant, got {downcast:?}"
591 );
592 }
593
594 #[test]
595 fn test_stream_token_debug() {
596 let token = StreamToken {
597 token: "hello".to_string(),
598 is_final: false,
599 };
600 assert!(format!("{:?}", token).contains("hello"));
601 }
602
603 #[tokio::test]
607 async fn test_default_stream_errors_on_non_string_output() {
608 struct NonStringChain;
609 #[async_trait]
610 impl BaseChain for NonStringChain {
611 fn input_keys(&self) -> Vec<&str> {
612 vec![]
613 }
614 fn output_keys(&self) -> Vec<&str> {
615 vec!["count"]
616 }
617 async fn invoke(
618 &self,
619 _inputs: HashMap<String, Value>,
620 ) -> Result<ChainResult, ChainError> {
621 let mut result = HashMap::new();
622 result.insert("count".to_string(), json!(3));
623 Ok(result)
624 }
625 }
626
627 let chain = NonStringChain;
628 let err = match chain.stream(HashMap::new()).await {
629 Ok(_) => panic!("expected an OutputError"),
630 Err(e) => e,
631 };
632 assert!(
633 matches!(err, ChainError::OutputError(_)),
634 "expected OutputError, got {err:?}"
635 );
636 }
637
638 #[test]
639 fn test_validate_inputs_pass() {
640 struct PassthroughChain;
641 #[async_trait]
642 impl BaseChain for PassthroughChain {
643 fn input_keys(&self) -> Vec<&str> {
644 vec!["input"]
645 }
646 fn output_keys(&self) -> Vec<&str> {
647 vec!["output"]
648 }
649 async fn invoke(
650 &self,
651 inputs: HashMap<String, Value>,
652 ) -> Result<ChainResult, ChainError> {
653 Ok(inputs)
654 }
655 }
656
657 let chain = PassthroughChain;
658 let mut inputs = HashMap::new();
659 inputs.insert("input".to_string(), Value::String("test".to_string()));
660 assert!(chain.validate_inputs(&inputs).is_ok());
661 }
662
663 #[test]
664 fn test_validate_inputs_missing_key() {
665 struct PassthroughChain;
666 #[async_trait]
667 impl BaseChain for PassthroughChain {
668 fn input_keys(&self) -> Vec<&str> {
669 vec!["input"]
670 }
671 fn output_keys(&self) -> Vec<&str> {
672 vec!["output"]
673 }
674 async fn invoke(
675 &self,
676 _inputs: HashMap<String, Value>,
677 ) -> Result<ChainResult, ChainError> {
678 Ok(HashMap::new())
679 }
680 }
681
682 let chain = PassthroughChain;
683 let inputs = HashMap::new();
684 assert!(chain.validate_inputs(&inputs).is_err());
685 }
686
687 #[test]
688 fn test_default_chain_name() {
689 struct MyChain;
690 #[async_trait]
691 impl BaseChain for MyChain {
692 fn input_keys(&self) -> Vec<&str> {
693 vec![]
694 }
695 fn output_keys(&self) -> Vec<&str> {
696 vec![]
697 }
698 async fn invoke(
699 &self,
700 _inputs: HashMap<String, Value>,
701 ) -> Result<ChainResult, ChainError> {
702 Ok(HashMap::new())
703 }
704 }
705 let chain = MyChain;
706 assert_eq!(chain.name(), "chain");
707 }
708}