lc_chains/document_chains/
map_reduce.rs1use async_trait::async_trait;
5use futures_util::future::try_join_all;
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};
14
15pub(crate) const DEFAULT_MAP_PROMPT: &str = "Answer the user's question based on the following document content. Provide a concise answer based on the document content.
17
18Document content:
19{context}
20
21Question: {input}
22
23Answer based on this document:";
24
25pub(crate) const DEFAULT_REDUCE_PROMPT: &str = "Below are answers from multiple documents. Please merge them into a single complete and coherent final answer.
27
28Answers from each document:
29{summaries}
30
31Original question: {input}
32
33Final consolidated answer:";
34
35pub struct MapReduceDocumentsChain<M: BaseChatModel> {
41 llm: M,
42 map_prompt_template: String,
43 reduce_prompt_template: String,
44 document_variable_name: String,
45 input_key: String,
46 output_key: String,
47 name: String,
48 verbose: bool,
49}
50
51impl<M: BaseChatModel> MapReduceDocumentsChain<M> {
52 pub fn new(llm: M) -> Self {
53 Self {
54 llm,
55 map_prompt_template: DEFAULT_MAP_PROMPT.to_string(),
56 reduce_prompt_template: DEFAULT_REDUCE_PROMPT.to_string(),
57 document_variable_name: "context".to_string(),
58 input_key: "input".to_string(),
59 output_key: "output".to_string(),
60 name: "map_reduce_documents".to_string(),
61 verbose: false,
62 }
63 }
64
65 pub fn with_map_prompt(mut self, template: impl Into<String>) -> Self {
66 self.map_prompt_template = template.into();
67 self
68 }
69
70 pub fn with_reduce_prompt(mut self, template: impl Into<String>) -> Self {
71 self.reduce_prompt_template = template.into();
72 self
73 }
74
75 pub fn with_document_variable(mut self, name: impl Into<String>) -> Self {
76 self.document_variable_name = name.into();
77 self
78 }
79
80 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
81 self.input_key = key.into();
82 self
83 }
84
85 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
86 self.output_key = key.into();
87 self
88 }
89
90 pub fn with_name(mut self, name: impl Into<String>) -> Self {
91 self.name = name.into();
92 self
93 }
94
95 pub fn with_verbose(mut self, verbose: bool) -> Self {
96 self.verbose = verbose;
97 self
98 }
99
100 pub fn build_map_prompt(&self, context: &str, input: &str) -> String {
101 self.map_prompt_template
102 .replace(&format!("{{{}}}", self.document_variable_name), context)
103 .replace("{input}", input)
104 }
105
106 pub fn build_reduce_prompt(&self, summaries: &[String], input: &str) -> String {
107 let summaries_text = summaries
108 .iter()
109 .enumerate()
110 .map(|(i, s)| format!("Answer from document {}:\n{}", i + 1, s))
111 .collect::<Vec<_>>()
112 .join("\n\n");
113
114 self.reduce_prompt_template
115 .replace("{summaries}", &summaries_text)
116 .replace("{input}", input)
117 }
118
119 async fn map_document(
121 &self,
122 doc: &Document,
123 input: &str,
124 index: usize,
125 ) -> Result<String, ChainError>
126 where
127 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
128 {
129 let prompt = self.build_map_prompt(&doc.content, input);
130
131 if self.verbose {
132 println!("\n--- Map document {} ---", index + 1);
133 }
134
135 let messages = vec![Message::human(&prompt)];
136 let response = self.llm.invoke(messages, None).await.map_err(|e| {
137 ChainError::ExecutionError(format!("Map call failed (document {}): {}", index + 1, e))
138 })?;
139
140 if self.verbose {
141 println!("Document {} answer: {}", index + 1, response.content);
142 }
143
144 Ok(response.content)
145 }
146
147 pub async fn invoke_with_documents(
149 &self,
150 documents: Vec<Document>,
151 input: &str,
152 ) -> Result<String, ChainError>
153 where
154 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
155 {
156 if documents.is_empty() {
157 return Err(ChainError::ExecutionError(
158 "Document list is empty".to_string(),
159 ));
160 }
161
162 if self.verbose {
163 println!("\n=== MapReduceDocumentsChain ===");
164 println!("Document count: {}", documents.len());
165 println!("Input: {}", input);
166 }
167
168 if self.verbose {
169 println!("\n--- Map phase ---");
170 }
171
172 let mut map_futures = Vec::new();
173 for (i, doc) in documents.iter().enumerate() {
174 map_futures.push(self.map_document(doc, input, i));
175 }
176 let summaries: Vec<String> = try_join_all(map_futures).await?;
177
178 if self.verbose {
179 println!("\n--- Reduce phase ---");
180 }
181
182 let reduce_prompt = self.build_reduce_prompt(&summaries, input);
183
184 if self.verbose {
185 println!("Merging answers from {} documents", summaries.len());
186 }
187
188 let messages = vec![Message::human(&reduce_prompt)];
189 let response = self
190 .llm
191 .invoke(messages, None)
192 .await
193 .map_err(|e| ChainError::ExecutionError(format!("Reduce call failed: {}", e)))?;
194
195 let final_answer = response.content;
196
197 if self.verbose {
198 println!("Final answer: {}", final_answer);
199 println!("=== MapReduceDocumentsChain complete ===\n");
200 }
201
202 Ok(final_answer)
203 }
204}
205
206#[async_trait]
207impl<M: BaseChatModel + Send + Sync + 'static> BaseChain for MapReduceDocumentsChain<M>
208where
209 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
210{
211 fn input_keys(&self) -> Vec<&str> {
212 vec![&self.input_key, "documents"]
213 }
214
215 fn output_keys(&self) -> Vec<&str> {
216 vec![&self.output_key]
217 }
218
219 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
220 let input = inputs
221 .get(&self.input_key)
222 .and_then(|v| v.as_str())
223 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
224
225 let documents: Vec<Document> = inputs
226 .get("documents")
227 .and_then(|v| v.as_array())
228 .map(|arr| {
229 arr.iter()
230 .filter_map(|v| serde_json::from_value(v.clone()).ok())
231 .collect()
232 })
233 .ok_or_else(|| ChainError::MissingInput("documents".to_string()))?;
234
235 let output = self.invoke_with_documents(documents, input).await?;
236
237 let mut result = HashMap::new();
238 result.insert(self.output_key.clone(), Value::String(output));
239 Ok(result)
240 }
241
242 fn name(&self) -> &str {
243 &self.name
244 }
245}