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