Skip to main content

langchain_rust/chain/combine_documents/
map_reduce.rs

1use futures::{stream, StreamExt, TryStreamExt};
2use serde_json::{json, Value};
3
4use crate::{
5    chain::{
6        stuff_documents::{
7            input_documents, COMBINE_DOCUMENTS_DEFAULT_DOCUMENT_VARIABLE_NAME,
8            COMBINE_DOCUMENTS_DEFAULT_INPUT_KEY,
9        },
10        Chain, ChainError, ChainEventStream,
11    },
12    language_models::{GenerateResult, TokenUsage},
13    prompt::PromptArgs,
14    schemas::Document,
15};
16
17use super::refine::{add_usage, shared_inputs, stream_last_step};
18
19const DEFAULT_MAX_CONCURRENCY: usize = 5;
20
21/// Combines documents in two phases: a map chain runs on every document independently
22/// (concurrently), then a reduce chain combines the mapped outputs.
23///
24/// Good for documents too large to fit in one prompt together, when each one can be
25/// processed on its own (summaries, extracting relevant passages).
26///
27/// Inputs: `input_documents` (a `Vec<Document>`) plus whatever the prompts use (e.g.
28/// `question`). The map chain gets each document's text as `context`. The reduce chain
29/// gets the mapped outputs as `input_documents` (keeping each source document's metadata),
30/// so a [`StuffDocument`](crate::chain::StuffDocument) works as the reduce chain.
31///
32/// ```rust,ignore
33/// let chain = MapReduceDocuments::load_summarize(OpenAI::default());
34/// let summary = chain
35///     .invoke(prompt_args! { "input_documents" => docs })
36///     .await?;
37/// ```
38pub struct MapReduceDocuments {
39    map_chain: Box<dyn Chain>,
40    reduce_chain: Box<dyn Chain>,
41    input_key: String,
42    document_variable_name: String,
43    reduce_input_key: String,
44    max_concurrency: usize,
45}
46
47impl MapReduceDocuments {
48    /// `map_chain` runs once per document; `reduce_chain` combines the results.
49    pub fn new<M: Into<Box<dyn Chain>>, R: Into<Box<dyn Chain>>>(
50        map_chain: M,
51        reduce_chain: R,
52    ) -> Self {
53        Self {
54            map_chain: map_chain.into(),
55            reduce_chain: reduce_chain.into(),
56            input_key: COMBINE_DOCUMENTS_DEFAULT_INPUT_KEY.into(),
57            document_variable_name: COMBINE_DOCUMENTS_DEFAULT_DOCUMENT_VARIABLE_NAME.into(),
58            reduce_input_key: COMBINE_DOCUMENTS_DEFAULT_INPUT_KEY.into(),
59            max_concurrency: DEFAULT_MAX_CONCURRENCY,
60        }
61    }
62
63    /// Input key holding the documents (default `input_documents`).
64    pub fn with_input_key<S: Into<String>>(mut self, key: S) -> Self {
65        self.input_key = key.into();
66        self
67    }
68
69    /// Prompt variable the map chain receives each document's text as (default `context`).
70    pub fn with_document_variable_name<S: Into<String>>(mut self, name: S) -> Self {
71        self.document_variable_name = name.into();
72        self
73    }
74
75    /// Input key the reduce chain receives the mapped documents in (default
76    /// `input_documents`).
77    pub fn with_reduce_input_key<S: Into<String>>(mut self, key: S) -> Self {
78        self.reduce_input_key = key.into();
79        self
80    }
81
82    /// How many map calls run at the same time (default 5, minimum 1).
83    pub fn with_max_concurrency(mut self, max_concurrency: usize) -> Self {
84        self.max_concurrency = max_concurrency.max(1);
85        self
86    }
87
88    /// Runs the map phase and returns the reduce chain's inputs plus the usage so far.
89    async fn map(
90        &self,
91        input_variables: &PromptArgs,
92    ) -> Result<(PromptArgs, Option<TokenUsage>), ChainError> {
93        let docs = input_documents(input_variables, &self.input_key)?;
94        let rest = shared_inputs(input_variables, &self.input_key);
95
96        // `buffered` keeps the output in document order.
97        let map_inputs: Vec<PromptArgs> = docs
98            .iter()
99            .map(|doc| {
100                let mut inputs = rest.clone();
101                inputs.insert(
102                    self.document_variable_name.clone(),
103                    Value::String(doc.page_content.clone()),
104                );
105                inputs
106            })
107            .collect();
108        let map_chain = self.map_chain.as_ref();
109        let results: Vec<GenerateResult> = stream::iter(map_inputs)
110            .map(|inputs| map_chain.call(inputs))
111            .buffered(self.max_concurrency)
112            .try_collect()
113            .await?;
114
115        let mut usage = None;
116        let mapped: Vec<Document> = docs
117            .into_iter()
118            .zip(results)
119            .map(|(doc, result)| {
120                add_usage(&mut usage, &result.tokens);
121                Document {
122                    page_content: result.generation,
123                    ..doc
124                }
125            })
126            .collect();
127
128        let mut reduce_inputs = rest;
129        reduce_inputs.insert(self.reduce_input_key.clone(), json!(mapped));
130        Ok((reduce_inputs, usage))
131    }
132}
133
134#[async_trait::async_trait]
135impl Chain for MapReduceDocuments {
136    async fn call(&self, input_variables: PromptArgs) -> Result<GenerateResult, ChainError> {
137        let (reduce_inputs, mut usage) = self.map(&input_variables).await?;
138        let mut result = self.reduce_chain.call(reduce_inputs).await?;
139        add_usage(&mut usage, &result.tokens);
140        result.tokens = usage;
141        Ok(result)
142    }
143
144    /// Runs the map phase quietly and streams the reduce step.
145    fn call_stream<'a>(&'a self, input_variables: PromptArgs) -> ChainEventStream<'a> {
146        Box::pin(
147            stream::once(async move {
148                match self.map(&input_variables).await {
149                    Ok((inputs, usage)) => {
150                        stream_last_step(self.reduce_chain.as_ref(), inputs, usage)
151                    }
152                    Err(e) => Box::pin(stream::iter([Err(e)])) as ChainEventStream<'a>,
153                }
154            })
155            .flatten(),
156        )
157    }
158
159    fn get_input_keys(&self) -> Vec<String> {
160        let mut keys = vec![self.input_key.clone()];
161        keys.extend(
162            self.map_chain
163                .get_input_keys()
164                .into_iter()
165                .filter(|k| *k != self.document_variable_name),
166        );
167        keys
168    }
169}