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 let context = self.format_documents(&documents);
125
126 if self.verbose {
127 println!("\n=== StuffDocumentsChain ===");
128 println!("Document count: {}", documents.len());
129 println!("Context length: {} characters", context.len());
130 }
131
132 let prompt = self.build_prompt(&context, input);
133
134 if self.verbose {
135 println!("Prompt length: {} characters", prompt.len());
136 }
137
138 let messages = vec![Message::human(&prompt)];
139 let response = self
140 .llm
141 .invoke(messages, None)
142 .await
143 .map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
144
145 let output = response.content;
146
147 if self.verbose {
148 println!("Output: {}", output);
149 println!("=== StuffDocumentsChain complete ===\n");
150 }
151
152 Ok(output)
153 }
154}
155
156#[async_trait]
157impl<M: BaseChatModel + Send + Sync + 'static> BaseChain for StuffDocumentsChain<M>
158where
159 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
160{
161 fn input_keys(&self) -> Vec<&str> {
162 vec![&self.input_key, "documents"]
163 }
164
165 fn output_keys(&self) -> Vec<&str> {
166 vec![&self.output_key]
167 }
168
169 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
170 let input = inputs
171 .get(&self.input_key)
172 .and_then(|v| v.as_str())
173 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
174
175 let documents: Vec<Document> = inputs
176 .get("documents")
177 .and_then(|v| v.as_array())
178 .map(|arr| {
179 arr.iter()
180 .filter_map(|v| serde_json::from_value(v.clone()).ok())
181 .collect()
182 })
183 .ok_or_else(|| ChainError::MissingInput("documents".to_string()))?;
184
185 let output = self.invoke_with_documents(documents, input).await?;
186
187 let mut result = HashMap::new();
188 result.insert(self.output_key.clone(), Value::String(output));
189 Ok(result)
190 }
191
192 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
197 self.validate_inputs(&inputs)?;
198
199 let input = inputs
200 .get(&self.input_key)
201 .and_then(|v| v.as_str())
202 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
203
204 let documents: Vec<Document> = inputs
205 .get("documents")
206 .and_then(|v| v.as_array())
207 .map(|arr| {
208 arr.iter()
209 .filter_map(|v| serde_json::from_value(v.clone()).ok())
210 .collect()
211 })
212 .ok_or_else(|| ChainError::MissingInput("documents".to_string()))?;
213
214 let context = self.format_documents(&documents);
215 let prompt = self.build_prompt(&context, input);
216 let messages = vec![Message::human(&prompt)];
217
218 let llm_stream = self
219 .llm
220 .stream_chat(messages, None)
221 .await
222 .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
223
224 let stream = llm_stream.map(|result| match result {
225 Ok(token) => Ok(StreamToken {
226 token,
227 is_final: false,
228 }),
229 Err(e) => Err(ChainError::StreamError(format!(
230 "Stream token error: {}",
231 e
232 ))),
233 });
234
235 let final_stream =
236 stream.chain(futures_util::stream::once(async move {
237 Ok(StreamToken {
238 token: String::new(),
239 is_final: true,
240 })
241 }));
242
243 Ok(Box::pin(final_stream))
244 }
245
246 fn name(&self) -> &str {
247 &self.name
248 }
249}