lc_chains/document_chains/
stuff.rs1use async_trait::async_trait;
5use futures_util::StreamExt;
6use lc_core::BaseChatModel;
7use lc_providers::{wrap_chat_model, ProviderError};
8use lc_schema::Message;
9use lc_shared::document::Document;
10use serde_json::Value;
11use std::collections::HashMap;
12
13use crate::base::{BaseChain, ChainError, ChainResult, ChainStream, StreamToken};
14use crate::BoxedChatModel;
15
16pub(crate) const DEFAULT_STUFF_PROMPT: &str =
18 "Answer the user's question based on the following reference information.
19
20Reference information:
21{context}
22
23Question: {input}
24
25Answer:";
26
27pub struct StuffDocumentsChain {
32 llm: BoxedChatModel,
33 prompt_template: String,
34 document_variable_name: String,
35 input_key: String,
36 output_key: String,
37 name: String,
38 verbose: bool,
39 max_doc_length: Option<usize>,
41}
42
43impl StuffDocumentsChain {
44 pub fn new<L>(llm: L) -> Self
46 where
47 L: BaseChatModel + Send + Sync + 'static,
48 L::Error: Into<ProviderError>,
49 {
50 Self {
51 llm: wrap_chat_model(llm),
52 prompt_template: DEFAULT_STUFF_PROMPT.to_string(),
53 document_variable_name: "context".to_string(),
54 input_key: "input".to_string(),
55 output_key: "output".to_string(),
56 name: "stuff_documents".to_string(),
57 verbose: false,
58 max_doc_length: None,
59 }
60 }
61
62 pub fn with_prompt_template(mut self, template: impl Into<String>) -> Self {
64 self.prompt_template = template.into();
65 self
66 }
67
68 pub fn with_document_variable(mut self, name: impl Into<String>) -> Self {
70 self.document_variable_name = name.into();
71 self
72 }
73
74 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
76 self.input_key = key.into();
77 self
78 }
79
80 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
82 self.output_key = key.into();
83 self
84 }
85
86 pub fn with_name(mut self, name: impl Into<String>) -> Self {
88 self.name = name.into();
89 self
90 }
91
92 pub fn with_verbose(mut self, verbose: bool) -> Self {
94 self.verbose = verbose;
95 self
96 }
97
98 pub fn with_max_doc_length(mut self, max: usize) -> Self {
100 self.max_doc_length = Some(max);
101 self
102 }
103
104 pub fn format_documents(&self, documents: &[Document]) -> String {
106 let mut parts = Vec::new();
107 for (i, doc) in documents.iter().enumerate() {
108 let mut content = doc.content.clone();
109 if let Some(max_len) = self.max_doc_length {
110 let char_count: usize = content.chars().count();
111 if char_count > max_len {
112 content = content.chars().take(max_len).collect::<String>();
113 content.push_str("...\n[document truncated]");
114 }
115 }
116 parts.push(format!("Document {}:\n{}", i + 1, content));
117 }
118 parts.join("\n\n---\n\n")
119 }
120
121 pub fn build_prompt(&self, context: &str, input: &str) -> String {
123 self.prompt_template
124 .replace(&format!("{{{}}}", self.document_variable_name), context)
125 .replace("{input}", input)
126 }
127
128 pub async fn invoke_with_documents(
130 &self,
131 documents: Vec<Document>,
132 input: &str,
133 ) -> Result<String, ChainError> {
134 if documents.is_empty() {
138 return Err(ChainError::ExecutionError(
139 "Document list is empty".to_string(),
140 ));
141 }
142
143 let context = self.format_documents(&documents);
144
145 if self.verbose {
146 println!("\n=== StuffDocumentsChain ===");
147 println!("Document count: {}", documents.len());
148 println!("Context length: {} characters", context.len());
149 }
150
151 let prompt = self.build_prompt(&context, input);
152
153 if self.verbose {
154 println!("Prompt length: {} characters", prompt.len());
155 }
156
157 let messages = vec![Message::human(&prompt)];
158 let response = self
159 .llm
160 .invoke(messages, None)
161 .await
162 .map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
163
164 let output = response.content;
165
166 if self.verbose {
167 println!("Output: {}", output);
168 println!("=== StuffDocumentsChain complete ===\n");
169 }
170
171 Ok(output)
172 }
173}
174
175#[async_trait]
176impl BaseChain for StuffDocumentsChain {
177 fn input_keys(&self) -> Vec<&str> {
178 vec![&self.input_key, "documents"]
179 }
180
181 fn output_keys(&self) -> Vec<&str> {
182 vec![&self.output_key]
183 }
184
185 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
186 self.validate_inputs(&inputs)?;
189
190 let input = inputs
191 .get(&self.input_key)
192 .and_then(|v| v.as_str())
193 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
194
195 let documents = crate::base::documents_from_input(inputs.get("documents"))?;
196
197 let output = self.invoke_with_documents(documents, input).await?;
198
199 let mut result = HashMap::new();
200 result.insert(self.output_key.clone(), Value::String(output));
201 Ok(result)
202 }
203
204 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
209 self.validate_inputs(&inputs)?;
210
211 let input = inputs
212 .get(&self.input_key)
213 .and_then(|v| v.as_str())
214 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
215
216 let documents = crate::base::documents_from_input(inputs.get("documents"))?;
217
218 if documents.is_empty() {
222 return Err(ChainError::ExecutionError(
223 "Document list is empty".to_string(),
224 ));
225 }
226
227 let context = self.format_documents(&documents);
228 let prompt = self.build_prompt(&context, input);
229 let messages = vec![Message::human(&prompt)];
230
231 let llm_stream = self
232 .llm
233 .stream_chat(messages, None)
234 .await
235 .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
236
237 let stream = llm_stream.map(|result| match result {
238 Ok(chunk) => Ok(StreamToken {
239 token: chunk.text,
240 is_final: false,
241 }),
242 Err(e) => Err(ChainError::StreamError(format!(
243 "Stream token error: {}",
244 e
245 ))),
246 });
247
248 let final_stream = stream.chain(futures_util::stream::once(async move {
249 Ok(StreamToken {
250 token: String::new(),
251 is_final: true,
252 })
253 }));
254
255 Ok(Box::pin(final_stream))
256 }
257
258 fn name(&self) -> &str {
259 &self.name
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use async_trait::async_trait;
267 use futures_util::Stream;
268 use lc_core::language_models::{LLMResult, StreamChunk};
269 use lc_core::runnables::RunnableConfig;
270 use lc_core::{BaseLanguageModel, Runnable};
271 use std::pin::Pin;
272 use std::sync::atomic::{AtomicUsize, Ordering};
273 use std::sync::Arc;
274
275 struct CountingLLM {
279 invokes: Arc<AtomicUsize>,
280 }
281
282 #[async_trait]
283 impl Runnable<Vec<Message>, LLMResult> for CountingLLM {
284 type Error = ProviderError;
285 async fn invoke(
286 &self,
287 _input: Vec<Message>,
288 _config: Option<RunnableConfig>,
289 ) -> Result<LLMResult, Self::Error> {
290 self.invokes.fetch_add(1, Ordering::SeqCst);
291 Ok(LLMResult {
292 content: "stuffed answer".to_string(),
293 model: "mock".to_string(),
294 token_usage: None,
295 tool_calls: None,
296 thinking_content: None,
297 })
298 }
299 }
300
301 #[async_trait]
302 impl BaseLanguageModel<Vec<Message>, LLMResult> for CountingLLM {
303 fn model_name(&self) -> &str {
304 "mock"
305 }
306 fn get_num_tokens(&self, t: &str) -> usize {
307 t.len()
308 }
309 fn with_temperature(self, _: f32) -> Self {
310 self
311 }
312 fn with_max_tokens(self, _: usize) -> Self {
313 self
314 }
315 }
316
317 #[async_trait]
318 impl BaseChatModel for CountingLLM {
319 async fn chat(
320 &self,
321 _messages: Vec<Message>,
322 _config: Option<RunnableConfig>,
323 ) -> Result<LLMResult, Self::Error> {
324 self.invokes.fetch_add(1, Ordering::SeqCst);
325 Ok(LLMResult {
326 content: "stuffed answer".to_string(),
327 model: "mock".to_string(),
328 token_usage: None,
329 tool_calls: None,
330 thinking_content: None,
331 })
332 }
333 async fn stream_chat(
334 &self,
335 _messages: Vec<Message>,
336 _config: Option<RunnableConfig>,
337 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk, Self::Error>> + Send>>, Self::Error>
338 {
339 let tokens = [Ok(StreamChunk::new("streamed answer"))];
340 Ok(Box::pin(futures_util::stream::iter(tokens)))
341 }
342 }
343
344 #[tokio::test]
347 async fn test_stuff_typed_invoke_runs_llm() {
348 let invokes = Arc::new(AtomicUsize::new(0));
349 let chain = StuffDocumentsChain::new(CountingLLM {
350 invokes: invokes.clone(),
351 });
352
353 let out = chain
354 .invoke_with_documents(vec![Document::new("doc one")], "question")
355 .await
356 .unwrap();
357 assert_eq!(out, "stuffed answer");
358 assert_eq!(invokes.load(Ordering::SeqCst), 1);
359 }
360
361 #[tokio::test]
365 async fn test_stuff_typed_invoke_empty_documents_errors() {
366 let invokes = Arc::new(AtomicUsize::new(0));
367 let chain = StuffDocumentsChain::new(CountingLLM {
368 invokes: invokes.clone(),
369 });
370
371 let err = match chain.invoke_with_documents(vec![], "q").await {
372 Ok(_) => panic!("expected an execution error"),
373 Err(e) => e,
374 };
375 assert!(matches!(err, ChainError::ExecutionError(_)));
376 assert_eq!(invokes.load(Ordering::SeqCst), 0);
377 }
378
379 #[tokio::test]
383 async fn test_stuff_invoke_missing_input_errors_before_llm() {
384 let invokes = Arc::new(AtomicUsize::new(0));
385 let chain = StuffDocumentsChain::new(CountingLLM {
386 invokes: invokes.clone(),
387 });
388 let mut inputs = HashMap::new();
389 inputs.insert("documents".to_string(), serde_json::json!([]));
390
391 let err = match chain.invoke(inputs).await {
392 Ok(_) => panic!("expected a missing-input error"),
393 Err(e) => e,
394 };
395 assert!(matches!(err, ChainError::MissingInput(_)));
396 assert_eq!(invokes.load(Ordering::SeqCst), 0);
397 }
398
399 #[tokio::test]
402 async fn test_stuff_stream_empty_documents_errors() {
403 let chain = StuffDocumentsChain::new(CountingLLM {
404 invokes: Arc::new(AtomicUsize::new(0)),
405 });
406 let mut inputs = HashMap::new();
407 inputs.insert("input".to_string(), Value::String("q".to_string()));
408 inputs.insert("documents".to_string(), serde_json::json!([]));
409
410 let err = match chain.stream(inputs).await {
411 Ok(_) => panic!("expected an execution error"),
412 Err(e) => e,
413 };
414 assert!(matches!(err, ChainError::ExecutionError(_)));
415 }
416}