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