Skip to main content

langchain_rust/chain/combine_documents/
refine.rs

1use futures::StreamExt;
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, LLMEvent, TokenUsage},
13    prompt::PromptArgs,
14    schemas::Document,
15};
16
17/// Adds `usage` to the running total, if the provider reported any.
18pub(super) fn add_usage(total: &mut Option<TokenUsage>, usage: &Option<TokenUsage>) {
19    if let Some(usage) = usage {
20        total.get_or_insert_with(TokenUsage::default).add(usage);
21    }
22}
23
24/// `input_variables` without the documents key: the extra inputs (e.g. `question`) that
25/// every step receives.
26pub(super) fn shared_inputs(input_variables: &PromptArgs, input_key: &str) -> PromptArgs {
27    let mut rest = input_variables.clone();
28    rest.remove(input_key);
29    rest
30}
31
32/// Runs `chain` with `call_stream`, adding `usage` (from earlier steps) to the final result.
33pub(super) fn stream_last_step<'a>(
34    chain: &'a dyn Chain,
35    inputs: PromptArgs,
36    usage: Option<TokenUsage>,
37) -> ChainEventStream<'a> {
38    Box::pin(chain.call_stream(inputs).map(move |event| match event {
39        Ok(LLMEvent::Done(mut result)) => {
40            let mut total = usage.clone();
41            add_usage(&mut total, &result.tokens);
42            result.tokens = total;
43            Ok(LLMEvent::Done(result))
44        }
45        other => other,
46    }))
47}
48
49/// Combines documents by running a chain on the first one, then refining that answer
50/// with each following document in turn.
51///
52/// Good when the documents don't fit in one prompt and order matters, or when you want
53/// the answer to build up as the model reads. It makes one LLM call per document,
54/// sequentially.
55///
56/// Inputs: `input_documents` (a `Vec<Document>`) plus whatever the prompts use (e.g.
57/// `question`). The initial chain gets each document as `context`; the refine chain also
58/// gets the previous answer as `existing_answer`.
59///
60/// ```rust,ignore
61/// let chain = RefineDocuments::load_summarize(OpenAI::default());
62/// let summary = chain
63///     .invoke(prompt_args! { "input_documents" => docs })
64///     .await?;
65/// ```
66pub struct RefineDocuments {
67    initial_chain: Box<dyn Chain>,
68    refine_chain: Box<dyn Chain>,
69    input_key: String,
70    document_variable_name: String,
71    initial_response_name: String,
72}
73
74impl RefineDocuments {
75    /// `initial_chain` answers from the first document; `refine_chain` takes
76    /// `existing_answer` and the next document and returns an improved answer.
77    pub fn new<C1: Into<Box<dyn Chain>>, C2: Into<Box<dyn Chain>>>(
78        initial_chain: C1,
79        refine_chain: C2,
80    ) -> Self {
81        Self {
82            initial_chain: initial_chain.into(),
83            refine_chain: refine_chain.into(),
84            input_key: COMBINE_DOCUMENTS_DEFAULT_INPUT_KEY.into(),
85            document_variable_name: COMBINE_DOCUMENTS_DEFAULT_DOCUMENT_VARIABLE_NAME.into(),
86            initial_response_name: "existing_answer".into(),
87        }
88    }
89
90    /// Input key holding the documents (default `input_documents`).
91    pub fn with_input_key<S: Into<String>>(mut self, key: S) -> Self {
92        self.input_key = key.into();
93        self
94    }
95
96    /// Prompt variable each document's text is passed as (default `context`).
97    pub fn with_document_variable_name<S: Into<String>>(mut self, name: S) -> Self {
98        self.document_variable_name = name.into();
99        self
100    }
101
102    /// Prompt variable the previous answer is passed as (default `existing_answer`).
103    pub fn with_initial_response_name<S: Into<String>>(mut self, name: S) -> Self {
104        self.initial_response_name = name.into();
105        self
106    }
107
108    fn step_inputs(&self, rest: &PromptArgs, doc: &Document, answer: Option<&str>) -> PromptArgs {
109        let mut inputs = rest.clone();
110        inputs.insert(
111            self.document_variable_name.clone(),
112            Value::String(doc.page_content.clone()),
113        );
114        if let Some(answer) = answer {
115            inputs.insert(self.initial_response_name.clone(), json!(answer));
116        }
117        inputs
118    }
119
120    /// Runs every step but the last one, returning the last step's chain, its inputs and
121    /// the usage so far.
122    async fn run_until_last(
123        &self,
124        input_variables: &PromptArgs,
125    ) -> Result<(&dyn Chain, PromptArgs, Option<TokenUsage>), ChainError> {
126        let docs = input_documents(input_variables, &self.input_key)?;
127        let Some((last, init)) = docs.split_last() else {
128            return Err(ChainError::OtherError(format!(
129                "{} must contain at least one document",
130                self.input_key
131            )));
132        };
133        let rest = shared_inputs(input_variables, &self.input_key);
134
135        let mut usage = None;
136        let mut answer: Option<String> = None;
137        for doc in init {
138            let (chain, inputs) = self.step(&rest, doc, answer.as_deref());
139            let result = chain.call(inputs).await?;
140            add_usage(&mut usage, &result.tokens);
141            answer = Some(result.generation);
142        }
143        let (chain, inputs) = self.step(&rest, last, answer.as_deref());
144        Ok((chain, inputs, usage))
145    }
146
147    /// The first document goes to the initial chain, the rest to the refine chain.
148    fn step(
149        &self,
150        rest: &PromptArgs,
151        doc: &Document,
152        answer: Option<&str>,
153    ) -> (&dyn Chain, PromptArgs) {
154        let chain = match answer {
155            None => self.initial_chain.as_ref(),
156            Some(_) => self.refine_chain.as_ref(),
157        };
158        (chain, self.step_inputs(rest, doc, answer))
159    }
160}
161
162#[async_trait::async_trait]
163impl Chain for RefineDocuments {
164    async fn call(&self, input_variables: PromptArgs) -> Result<GenerateResult, ChainError> {
165        let (chain, inputs, mut usage) = self.run_until_last(&input_variables).await?;
166        let mut result = chain.call(inputs).await?;
167        add_usage(&mut usage, &result.tokens);
168        result.tokens = usage;
169        Ok(result)
170    }
171
172    /// Runs the intermediate steps quietly and streams the final refinement.
173    fn call_stream<'a>(&'a self, input_variables: PromptArgs) -> ChainEventStream<'a> {
174        Box::pin(
175            futures::stream::once(async move {
176                match self.run_until_last(&input_variables).await {
177                    Ok((chain, inputs, usage)) => stream_last_step(chain, inputs, usage),
178                    Err(e) => Box::pin(futures::stream::iter([Err(e)])) as ChainEventStream<'a>,
179                }
180            })
181            .flatten(),
182        )
183    }
184
185    fn get_input_keys(&self) -> Vec<String> {
186        let mut keys = vec![self.input_key.clone()];
187        keys.extend(
188            self.initial_chain
189                .get_input_keys()
190                .into_iter()
191                .filter(|k| *k != self.document_variable_name),
192        );
193        keys
194    }
195}