Skip to main content

lc_chains/
base.rs

1// lc-chains/src/base.rs
2//! Chain base trait.
3
4use 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/// Chain error type.
17#[derive(Debug, thiserror::Error)]
18pub enum ChainError {
19    /// Missing input.
20    #[error("Missing input: {0}")]
21    MissingInput(String),
22
23    /// Input error (present but malformed — e.g. a document that fails to deserialize).
24    #[error("Input error: {0}")]
25    InputError(String),
26
27    /// Output error.
28    #[error("Output error: {0}")]
29    OutputError(String),
30
31    /// Execution error.
32    #[error("Execution error: {0}")]
33    ExecutionError(String),
34
35    /// Stream error.
36    #[error("Stream error: {0}")]
37    StreamError(String),
38
39    /// Other error.
40    #[error("Chain error: {0}")]
41    Other(String),
42
43    /// Nested (sub-chain / LLM) execution error, preserving the original error
44    /// chain.
45    ///
46    /// P2-1: composite chains (SequentialChain / RouterChain) wrap sub-chain or
47    /// LLM failures in this variant so the underlying error stays inspectable
48    /// via `source()` (e.g. downcast back to a concrete `ChainError` variant)
49    /// instead of being flattened into a string by `format!`.
50    #[error("{context}: {source}")]
51    Nested {
52        /// Human-readable context describing which step/chain failed.
53        context: String,
54        /// The underlying error, preserved for chaining.
55        #[source]
56        source: Box<dyn std::error::Error + Send + Sync>,
57    },
58}
59
60/// Chain execution result.
61pub type ChainResult = HashMap<String, Value>;
62
63/// Stream output item: token-by-token output.
64#[derive(Debug, Clone)]
65pub struct StreamToken {
66    /// Token text.
67    pub token: String,
68    /// Whether this is the final token.
69    pub is_final: bool,
70}
71
72/// Chain stream output type.
73pub type ChainStream = Pin<Box<dyn Stream<Item = Result<StreamToken, ChainError>> + Send>>;
74
75/// Convert memory variables (from `BaseMemory::load_memory_variables`) into a
76/// message list for LLM consumption.
77///
78/// Memory implementations produce two shapes under their variable keys:
79/// - `Value::Array` of serialized [`Message`] objects (`return_messages = true`)
80/// - `Value::String` rendered history (return_messages = false, summary, vectorstore)
81///
82/// String-form history is wrapped as a `System` message, matching the convention
83/// used by `ConversationSummaryMemory` (summary.rs wraps its buffer as System).
84pub(crate) fn variables_to_messages(vars: &HashMap<String, Value>) -> Vec<Message> {
85    // P2-1: 统一收敛到 lc-memory 的公共转换,避免两处实现漂移。
86    lc_memory::memory_variables_to_messages(vars)
87}
88
89/// Deserialize a `documents` input array into `Vec<Document>`, failing loudly
90/// when any item cannot be parsed instead of silently dropping it.
91///
92/// P1-2: replaces the old `filter_map(|v| serde_json::from_value(v.clone()).ok())`
93/// pattern, which hid malformed entries from the caller. A missing `documents`
94/// key, or a present-but-malformed array, yields [`ChainError::MissingInput`] /
95/// [`ChainError::InputError`] respectively.
96pub(crate) fn documents_from_input(value: Option<&Value>) -> Result<Vec<Document>, ChainError> {
97    let arr = value
98        .and_then(|v| v.as_array())
99        .ok_or_else(|| ChainError::MissingInput("documents".to_string()))?;
100
101    let mut docs = Vec::with_capacity(arr.len());
102    let mut failed = 0usize;
103    for item in arr {
104        match serde_json::from_value::<Document>(item.clone()) {
105            Ok(doc) => docs.push(doc),
106            Err(_) => failed += 1,
107        }
108    }
109    if failed > 0 {
110        return Err(ChainError::InputError(format!(
111            "document deserialization failed: {failed} of {} document(s) lost",
112            arr.len()
113        )));
114    }
115    Ok(docs)
116}
117
118/// Serialize documents for the `source_documents` output key, failing loudly
119/// instead of silently inserting `Value::Null` entries.
120pub(crate) fn documents_to_values(documents: &[Document]) -> Result<Vec<Value>, ChainError> {
121    documents
122        .iter()
123        .map(|doc| {
124            serde_json::to_value(doc)
125                .map_err(|e| ChainError::Other(format!("failed to serialize document: {e}")))
126        })
127        .collect()
128}
129
130/// Base Chain trait.
131///
132/// Chain is LangChain's core abstraction, representing a sequence of operations.
133#[async_trait]
134pub trait BaseChain: Send + Sync {
135    /// Get input keys.
136    fn input_keys(&self) -> Vec<&str>;
137
138    /// Get output keys.
139    fn output_keys(&self) -> Vec<&str>;
140
141    /// Execute the Chain.
142    ///
143    /// # Arguments
144    /// * `inputs` - Input parameter dictionary
145    ///
146    /// # Returns
147    /// Output result dictionary
148    async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError>;
149
150    /// Execute the Chain with a RunnableConfig.
151    ///
152    /// This method propagates callbacks through the chain execution.
153    /// The default implementation wraps `invoke()` with `on_chain_start` /
154    /// `on_chain_end` (or `on_chain_error`) dispatch, so config.callbacks are
155    /// never silently dropped — even for chains that don't override this method.
156    ///
157    /// Chains that want to propagate LLM callbacks (on_llm_start/end) or thread
158    /// config into sub-chains (SequentialChain / RouterChain) override this method.
159    async fn invoke_with_config(
160        &self,
161        inputs: HashMap<String, Value>,
162        config: Option<RunnableConfig>,
163    ) -> Result<ChainResult, ChainError> {
164        run_chain_with_callbacks(self.name(), inputs, config, |inputs| async move {
165            self.invoke(inputs).await
166        })
167        .await
168    }
169
170    /// Stream execute the Chain -- token by token output.
171    ///
172    /// Default implementation wraps the invoke result as a single-element stream.
173    /// Chains that support LLM streaming (LLMChain / ConversationChain) should
174    /// override this method, calling `BaseChatModel::stream_chat` internally.
175    ///
176    /// # Arguments
177    /// * `inputs` - Input parameter dictionary
178    ///
179    /// # Returns
180    /// Token stream
181    async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
182        // Default: wrap invoke result as single-element stream
183        let result = self.invoke(inputs).await?;
184        // P2-2: a chain that produces no string output now fails loudly instead
185        // of silently streaming a single empty token (`unwrap_or("")`).
186        let output_text = result
187            .values()
188            .next()
189            .and_then(|v| v.as_str())
190            .ok_or_else(|| {
191                ChainError::OutputError("chain produced no string output to stream".to_string())
192            })?
193            .to_string();
194        let stream = futures_util::stream::once(async move {
195            Ok(StreamToken {
196                token: output_text,
197                is_final: true,
198            })
199        });
200        Ok(Box::pin(stream))
201    }
202
203    /// Stream execute the Chain with a RunnableConfig.
204    ///
205    /// The default implementation wraps `stream()` with `on_chain_start` and
206    /// dispatches `on_chain_end` (or `on_chain_error`) once the token stream
207    /// completes, so config.callbacks are never silently dropped.
208    async fn stream_with_config(
209        &self,
210        inputs: HashMap<String, Value>,
211        config: Option<RunnableConfig>,
212    ) -> Result<ChainStream, ChainError> {
213        stream_chain_with_callbacks(self.name(), inputs, config, |inputs| async move {
214            self.stream(inputs).await
215        })
216        .await
217    }
218
219    /// Validate inputs.
220    fn validate_inputs(&self, inputs: &HashMap<String, Value>) -> Result<(), ChainError> {
221        for key in self.input_keys() {
222            if !inputs.contains_key(key) {
223                return Err(ChainError::MissingInput(key.to_string()));
224            }
225        }
226        Ok(())
227    }
228
229    /// Get Chain name.
230    fn name(&self) -> &str {
231        "chain"
232    }
233}
234
235/// Run a chain body wrapped in `on_chain_start` → body → `on_chain_end` /
236/// `on_chain_error` callback dispatch.
237///
238/// Shared by the default `invoke_with_config` and by composite chains
239/// (SequentialChain / RouterChain) that override it to thread `config` into
240/// their sub-chains. Callbacks are only dispatched when `config` carries one;
241/// otherwise this is a plain pass-through so the common path pays no tracing.
242pub(crate) async fn run_chain_with_callbacks<F, Fut>(
243    name: &str,
244    inputs: HashMap<String, Value>,
245    config: Option<RunnableConfig>,
246    body: F,
247) -> Result<ChainResult, ChainError>
248where
249    F: FnOnce(HashMap<String, Value>) -> Fut,
250    Fut: Future<Output = Result<ChainResult, ChainError>> + Send,
251{
252    let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
253    let mut run = RunTree::new(name, RunType::Chain, json!({ "inputs": inputs }));
254
255    if let Some(ref cb) = callbacks {
256        cb.dispatch_chain_start(&run, &run.inputs).await;
257    }
258
259    let result = body(inputs).await;
260
261    match result {
262        Ok(output) => {
263            run.end(json!({ "output": output }));
264            if let Some(ref cb) = callbacks {
265                cb.dispatch_chain_end(&run, &json!({ "output": output }))
266                    .await;
267            }
268            Ok(output)
269        }
270        Err(e) => {
271            let msg = e.to_string();
272            run.end_with_error(msg.clone());
273            if let Some(ref cb) = callbacks {
274                cb.dispatch_chain_error(&run, &msg).await;
275            }
276            Err(e)
277        }
278    }
279}
280
281/// Stream a chain body wrapped in `on_chain_start` dispatch, ending the run
282/// (and dispatching `on_chain_end` / `on_chain_error`) once the token stream
283/// completes or fails.
284///
285/// Shared by the default `stream_with_config` and by composite chains that
286/// override it to thread `config` into their sub-chains.
287pub(crate) async fn stream_chain_with_callbacks<F, Fut>(
288    name: &str,
289    inputs: HashMap<String, Value>,
290    config: Option<RunnableConfig>,
291    body: F,
292) -> Result<ChainStream, ChainError>
293where
294    F: FnOnce(HashMap<String, Value>) -> Fut,
295    Fut: Future<Output = Result<ChainStream, ChainError>> + Send,
296{
297    let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
298    let mut run = RunTree::new(name, RunType::Chain, json!({ "inputs": inputs }));
299
300    if let Some(ref cb) = callbacks {
301        cb.dispatch_chain_start(&run, &run.inputs).await;
302    }
303
304    let stream = match body(inputs).await {
305        Ok(s) => s,
306        Err(e) => {
307            let msg = e.to_string();
308            run.end_with_error(msg.clone());
309            if let Some(ref cb) = callbacks {
310                cb.dispatch_chain_error(&run, &msg).await;
311            }
312            return Err(e);
313        }
314    };
315
316    Ok(Box::pin(end_stream_on_completion(stream, run, callbacks)))
317}
318
319/// Wrap a chain token stream so the RunTree is ended and `on_chain_end` /
320/// `on_chain_error` dispatched once the stream completes or errors.
321fn end_stream_on_completion(
322    inner: ChainStream,
323    run: RunTree,
324    callbacks: Option<Arc<CallbackManager>>,
325) -> impl Stream<Item = Result<StreamToken, ChainError>> + Send {
326    stream::unfold(Some((inner, run, callbacks)), |state| async move {
327        let (mut inner, run, callbacks) = match state {
328            Some(s) => s,
329            None => return None,
330        };
331        match inner.next().await {
332            Some(Ok(token)) => Some((Ok(token), Some((inner, run, callbacks)))),
333            Some(Err(e)) => {
334                let msg = e.to_string();
335                let mut run = run;
336                run.end_with_error(msg.clone());
337                if let Some(cb) = callbacks {
338                    cb.dispatch_chain_error(&run, &msg).await;
339                }
340                Some((Err(e), None))
341            }
342            None => {
343                let mut run = run;
344                run.end(json!({ "output": null }));
345                if let Some(cb) = callbacks {
346                    cb.dispatch_chain_end(&run, &json!({ "output": null }))
347                        .await;
348                }
349                None
350            }
351        }
352    })
353}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use std::error::Error;
359
360    #[test]
361    fn test_chain_error_display() {
362        let error = ChainError::MissingInput("test".to_string());
363        assert!(error.to_string().contains("Missing input"));
364
365        let error = ChainError::ExecutionError("test".to_string());
366        assert!(error.to_string().contains("Execution error"));
367    }
368
369    #[test]
370    fn test_chain_error_all_variants() {
371        let err = ChainError::MissingInput("key".to_string());
372        assert!(err.to_string().contains("key"));
373
374        let err = ChainError::OutputError("bad".to_string());
375        assert!(err.to_string().contains("bad"));
376
377        let err = ChainError::ExecutionError("fail".to_string());
378        assert!(err.to_string().contains("fail"));
379
380        let err = ChainError::StreamError("broken".to_string());
381        assert!(err.to_string().contains("broken"));
382
383        let err = ChainError::Other("misc".to_string());
384        assert!(err.to_string().contains("misc"));
385    }
386
387    /// P2-1: `Nested` preserves the original error chain — `source()` must
388    /// downcast back to the concrete `ChainError` variant instead of a
389    /// flattened string.
390    #[test]
391    fn test_chain_error_nested_preserves_source() {
392        let inner = ChainError::MissingInput("text".to_string());
393        let nested = ChainError::Nested {
394            context: "Step 0 (echo) execution failed".to_string(),
395            source: Box::new(inner),
396        };
397        assert!(nested
398            .to_string()
399            .contains("Step 0 (echo) execution failed"));
400        assert!(nested.to_string().contains("Missing input"));
401
402        let source = nested.source().expect("Nested must carry a source");
403        let downcast = source.downcast_ref::<ChainError>();
404        assert!(
405            matches!(downcast, Some(ChainError::MissingInput(k)) if k == "text"),
406            "source should downcast back to the original variant, got {downcast:?}"
407        );
408    }
409
410    #[test]
411    fn test_stream_token_debug() {
412        let token = StreamToken {
413            token: "hello".to_string(),
414            is_final: false,
415        };
416        assert!(format!("{:?}", token).contains("hello"));
417    }
418
419    /// P2-2: the default stream fails loudly when the chain produces a
420    /// non-string output instead of silently emitting a single empty token
421    /// (the old `unwrap_or("")`).
422    #[tokio::test]
423    async fn test_default_stream_errors_on_non_string_output() {
424        struct NonStringChain;
425        #[async_trait]
426        impl BaseChain for NonStringChain {
427            fn input_keys(&self) -> Vec<&str> {
428                vec![]
429            }
430            fn output_keys(&self) -> Vec<&str> {
431                vec!["count"]
432            }
433            async fn invoke(
434                &self,
435                _inputs: HashMap<String, Value>,
436            ) -> Result<ChainResult, ChainError> {
437                let mut result = HashMap::new();
438                result.insert("count".to_string(), json!(3));
439                Ok(result)
440            }
441        }
442
443        let chain = NonStringChain;
444        let err = match chain.stream(HashMap::new()).await {
445            Ok(_) => panic!("expected an OutputError"),
446            Err(e) => e,
447        };
448        assert!(
449            matches!(err, ChainError::OutputError(_)),
450            "expected OutputError, got {err:?}"
451        );
452    }
453
454    #[test]
455    fn test_validate_inputs_pass() {
456        struct PassthroughChain;
457        #[async_trait]
458        impl BaseChain for PassthroughChain {
459            fn input_keys(&self) -> Vec<&str> {
460                vec!["input"]
461            }
462            fn output_keys(&self) -> Vec<&str> {
463                vec!["output"]
464            }
465            async fn invoke(
466                &self,
467                inputs: HashMap<String, Value>,
468            ) -> Result<ChainResult, ChainError> {
469                Ok(inputs)
470            }
471        }
472
473        let chain = PassthroughChain;
474        let mut inputs = HashMap::new();
475        inputs.insert("input".to_string(), Value::String("test".to_string()));
476        assert!(chain.validate_inputs(&inputs).is_ok());
477    }
478
479    #[test]
480    fn test_validate_inputs_missing_key() {
481        struct PassthroughChain;
482        #[async_trait]
483        impl BaseChain for PassthroughChain {
484            fn input_keys(&self) -> Vec<&str> {
485                vec!["input"]
486            }
487            fn output_keys(&self) -> Vec<&str> {
488                vec!["output"]
489            }
490            async fn invoke(
491                &self,
492                _inputs: HashMap<String, Value>,
493            ) -> Result<ChainResult, ChainError> {
494                Ok(HashMap::new())
495            }
496        }
497
498        let chain = PassthroughChain;
499        let inputs = HashMap::new();
500        assert!(chain.validate_inputs(&inputs).is_err());
501    }
502
503    #[test]
504    fn test_default_chain_name() {
505        struct MyChain;
506        #[async_trait]
507        impl BaseChain for MyChain {
508            fn input_keys(&self) -> Vec<&str> {
509                vec![]
510            }
511            fn output_keys(&self) -> Vec<&str> {
512                vec![]
513            }
514            async fn invoke(
515                &self,
516                _inputs: HashMap<String, Value>,
517            ) -> Result<ChainResult, ChainError> {
518                Ok(HashMap::new())
519            }
520        }
521        let chain = MyChain;
522        assert_eq!(chain.name(), "chain");
523    }
524}