lc_chains/document_chains/
refine.rs1use async_trait::async_trait;
5use futures_util::StreamExt;
6use lc_core::BaseChatModel;
7use lc_providers::{wrap_chat_model, ProviderError};
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};
14use crate::BoxedChatModel;
15
16pub(crate) const DEFAULT_REFINE_INITIAL_PROMPT: &str =
18 "Answer the question based on the following reference information.
19
20Reference information:
21{context}
22
23Question: {input}
24
25Answer:";
26
27pub(crate) const DEFAULT_REFINE_PROMPT: &str = "You have provided an answer based on partial information. Here is additional reference information.
29
30Existing answer:
31{existing_answer}
32
33New reference information:
34{context}
35
36Please refine or modify your answer based on the new information. If the new information does not conflict with the existing answer, merge them. If the new information conflicts with the existing answer, prioritize the new information.
37
38Question: {input}
39
40Refined answer:";
41
42pub struct RefineDocumentsChain {
47 llm: BoxedChatModel,
48 initial_prompt_template: String,
49 refine_prompt_template: String,
50 document_variable_name: String,
51 input_key: String,
52 output_key: String,
53 name: String,
54 verbose: bool,
55}
56
57impl RefineDocumentsChain {
58 pub fn new<L>(llm: L) -> Self
60 where
61 L: BaseChatModel + Send + Sync + 'static,
62 L::Error: Into<ProviderError>,
63 {
64 Self {
65 llm: wrap_chat_model(llm),
66 initial_prompt_template: DEFAULT_REFINE_INITIAL_PROMPT.to_string(),
67 refine_prompt_template: DEFAULT_REFINE_PROMPT.to_string(),
68 document_variable_name: "context".to_string(),
69 input_key: "input".to_string(),
70 output_key: "output".to_string(),
71 name: "refine_documents".to_string(),
72 verbose: false,
73 }
74 }
75
76 pub fn with_initial_prompt(mut self, template: impl Into<String>) -> Self {
78 self.initial_prompt_template = template.into();
79 self
80 }
81
82 pub fn with_refine_prompt(mut self, template: impl Into<String>) -> Self {
84 self.refine_prompt_template = template.into();
85 self
86 }
87
88 pub fn with_document_variable(mut self, name: impl Into<String>) -> Self {
90 self.document_variable_name = name.into();
91 self
92 }
93
94 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
96 self.input_key = key.into();
97 self
98 }
99
100 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
102 self.output_key = key.into();
103 self
104 }
105
106 pub fn with_name(mut self, name: impl Into<String>) -> Self {
108 self.name = name.into();
109 self
110 }
111
112 pub fn with_verbose(mut self, verbose: bool) -> Self {
114 self.verbose = verbose;
115 self
116 }
117
118 pub fn build_initial_prompt(&self, context: &str, input: &str) -> String {
120 self.initial_prompt_template
121 .replace(&format!("{{{}}}", self.document_variable_name), context)
122 .replace("{input}", input)
123 }
124
125 pub fn build_refine_prompt(&self, context: &str, input: &str, existing_answer: &str) -> String {
127 self.refine_prompt_template
128 .replace(&format!("{{{}}}", self.document_variable_name), context)
129 .replace("{input}", input)
130 .replace("{existing_answer}", existing_answer)
131 }
132
133 pub async fn invoke_with_documents(
135 &self,
136 documents: Vec<Document>,
137 input: &str,
138 ) -> Result<String, ChainError> {
139 if documents.is_empty() {
140 return Err(ChainError::ExecutionError(
141 "Document list is empty".to_string(),
142 ));
143 }
144
145 if self.verbose {
146 println!("\n=== RefineDocumentsChain ===");
147 println!("Document count: {}", documents.len());
148 println!("Input: {}", input);
149 }
150
151 let first_context = &documents[0].content;
153 let initial_prompt = self.build_initial_prompt(first_context, input);
154
155 if self.verbose {
156 println!("\n--- Initial processing (document 1) ---");
157 }
158
159 let messages = vec![Message::human(&initial_prompt)];
160 let response =
161 self.llm.invoke(messages, None).await.map_err(|e| {
162 ChainError::ExecutionError(format!("LLM initial call failed: {}", e))
163 })?;
164 let mut answer = response.content;
165
166 if self.verbose {
167 println!("Initial answer: {}", answer);
168 }
169
170 for (i, doc) in documents[1..].iter().enumerate() {
172 if self.verbose {
173 println!("\n--- Refinement step {} (document {}) ---", i + 1, i + 2);
174 }
175
176 let refine_prompt = self.build_refine_prompt(&doc.content, input, &answer);
177
178 let messages = vec![Message::human(&refine_prompt)];
179 let response = self.llm.invoke(messages, None).await.map_err(|e| {
180 ChainError::ExecutionError(format!("LLM refinement call failed: {}", e))
181 })?;
182 answer = response.content;
183
184 if self.verbose {
185 println!("Refined answer: {}", answer);
186 }
187 }
188
189 if self.verbose {
190 println!("=== RefineDocumentsChain complete ===\n");
191 }
192
193 Ok(answer)
194 }
195}
196
197#[async_trait]
198impl BaseChain for RefineDocumentsChain {
199 fn input_keys(&self) -> Vec<&str> {
200 vec![&self.input_key, "documents"]
201 }
202
203 fn output_keys(&self) -> Vec<&str> {
204 vec![&self.output_key]
205 }
206
207 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
208 self.validate_inputs(&inputs)?;
210
211 let input = inputs
212 .get(&self.input_key)
213 .and_then(|v| v.as_str())
214 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
215
216 let documents = crate::base::documents_from_input(inputs.get("documents"))?;
217
218 let output = self.invoke_with_documents(documents, input).await?;
219
220 let mut result = HashMap::new();
221 result.insert(self.output_key.clone(), Value::String(output));
222 Ok(result)
223 }
224
225 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
233 self.validate_inputs(&inputs)?;
234
235 let input = inputs
236 .get(&self.input_key)
237 .and_then(|v| v.as_str())
238 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
239
240 let documents = crate::base::documents_from_input(inputs.get("documents"))?;
241
242 if documents.is_empty() {
243 return Err(ChainError::ExecutionError(
244 "Document list is empty".to_string(),
245 ));
246 }
247
248 let first_context = &documents[0].content;
250 let initial_prompt = self.build_initial_prompt(first_context, input);
251 let messages = vec![Message::human(&initial_prompt)];
252 let response =
253 self.llm.invoke(messages, None).await.map_err(|e| {
254 ChainError::ExecutionError(format!("LLM initial call failed: {}", e))
255 })?;
256 let mut answer = response.content;
257
258 let last_idx = documents.len() - 1;
265 for (i, doc) in documents
266 .iter()
267 .skip(1)
268 .take(last_idx.saturating_sub(1))
269 .enumerate()
270 {
271 let refine_prompt = self.build_refine_prompt(&doc.content, input, &answer);
272 let messages = vec![Message::human(&refine_prompt)];
273 let response = self.llm.invoke(messages, None).await.map_err(|e| {
274 ChainError::ExecutionError(format!("LLM refinement call failed: {}", e))
275 })?;
276 answer = response.content;
277
278 if self.verbose {
279 println!("Refine step {} completed", i + 1);
280 }
281 }
282
283 if last_idx == 0 {
291 let stream = futures_util::stream::once(async move {
292 Ok(StreamToken {
293 token: answer,
294 is_final: true,
295 })
296 });
297 return Ok(Box::pin(stream));
298 }
299
300 let final_prompt = self.build_refine_prompt(&documents[last_idx].content, input, &answer);
301
302 let messages = vec![Message::human(&final_prompt)];
303 let llm_stream = self
304 .llm
305 .stream_chat(messages, None)
306 .await
307 .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
308
309 let stream = llm_stream.map(|result| match result {
310 Ok(token) => Ok(StreamToken {
311 token,
312 is_final: false,
313 }),
314 Err(e) => Err(ChainError::StreamError(format!(
315 "Stream token error: {}",
316 e
317 ))),
318 });
319
320 let final_stream = stream.chain(futures_util::stream::once(async move {
321 Ok(StreamToken {
322 token: String::new(),
323 is_final: true,
324 })
325 }));
326
327 Ok(Box::pin(final_stream))
328 }
329
330 fn name(&self) -> &str {
331 &self.name
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use async_trait::async_trait;
339 use futures_util::Stream;
340 use lc_core::language_models::LLMResult;
341 use lc_core::runnables::RunnableConfig;
342 use lc_core::{BaseLanguageModel, Runnable};
343 use std::pin::Pin;
344 use std::sync::atomic::{AtomicUsize, Ordering};
345 use std::sync::Arc;
346
347 struct CountingLLM {
350 invokes: Arc<AtomicUsize>,
351 streams: Arc<AtomicUsize>,
352 }
353
354 #[async_trait]
355 impl Runnable<Vec<Message>, LLMResult> for CountingLLM {
356 type Error = ProviderError;
357 async fn invoke(
358 &self,
359 _input: Vec<Message>,
360 _config: Option<RunnableConfig>,
361 ) -> Result<LLMResult, Self::Error> {
362 self.invokes.fetch_add(1, Ordering::SeqCst);
363 Ok(LLMResult {
364 content: "initial answer".to_string(),
365 model: "mock".to_string(),
366 token_usage: None,
367 tool_calls: None,
368 thinking_content: None,
369 })
370 }
371 }
372
373 #[async_trait]
374 impl BaseLanguageModel<Vec<Message>, LLMResult> for CountingLLM {
375 fn model_name(&self) -> &str {
376 "mock"
377 }
378 fn get_num_tokens(&self, t: &str) -> usize {
379 t.len()
380 }
381 fn with_temperature(self, _: f32) -> Self {
382 self
383 }
384 fn with_max_tokens(self, _: usize) -> Self {
385 self
386 }
387 }
388
389 #[async_trait]
390 impl BaseChatModel for CountingLLM {
391 async fn chat(
392 &self,
393 _messages: Vec<Message>,
394 _config: Option<RunnableConfig>,
395 ) -> Result<LLMResult, Self::Error> {
396 self.invokes.fetch_add(1, Ordering::SeqCst);
397 Ok(LLMResult {
398 content: "initial answer".to_string(),
399 model: "mock".to_string(),
400 token_usage: None,
401 tool_calls: None,
402 thinking_content: None,
403 })
404 }
405 async fn stream_chat(
406 &self,
407 _messages: Vec<Message>,
408 _config: Option<RunnableConfig>,
409 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error>
410 {
411 self.streams.fetch_add(1, Ordering::SeqCst);
412 let tokens = [Ok("refined answer".to_string())];
413 Ok(Box::pin(futures_util::stream::iter(tokens)))
414 }
415 }
416
417 fn inputs_for(documents: Vec<Document>) -> HashMap<String, Value> {
418 let mut inputs = HashMap::new();
419 inputs.insert("input".to_string(), Value::String("question".to_string()));
420 inputs.insert(
421 "documents".to_string(),
422 serde_json::to_value(documents).unwrap(),
423 );
424 inputs
425 }
426
427 #[tokio::test]
431 async fn test_refine_stream_single_document_skips_second_llm_call() {
432 let invokes = Arc::new(AtomicUsize::new(0));
433 let streams = Arc::new(AtomicUsize::new(0));
434 let chain = RefineDocumentsChain::new(CountingLLM {
435 invokes: invokes.clone(),
436 streams: streams.clone(),
437 });
438 let inputs = inputs_for(vec![Document::new("doc one")]);
439
440 let mut stream = chain.stream(inputs).await.unwrap();
441 let mut tokens = Vec::new();
442 while let Some(item) = stream.next().await {
443 tokens.push(item.unwrap());
444 }
445 let text: String = tokens.iter().map(|t| t.token.as_str()).collect();
446 assert_eq!(text, "initial answer");
447 assert!(tokens.last().unwrap().is_final);
448 assert_eq!(invokes.load(Ordering::SeqCst), 1, "one initial invoke");
449 assert_eq!(
450 streams.load(Ordering::SeqCst),
451 0,
452 "single-document stream must not re-call the LLM"
453 );
454 }
455
456 #[tokio::test]
459 async fn test_refine_stream_multi_document_streams_final_refine() {
460 let invokes = Arc::new(AtomicUsize::new(0));
461 let streams = Arc::new(AtomicUsize::new(0));
462 let chain = RefineDocumentsChain::new(CountingLLM {
463 invokes: invokes.clone(),
464 streams: streams.clone(),
465 });
466 let inputs = inputs_for(vec![
467 Document::new("doc one"),
468 Document::new("doc two"),
469 Document::new("doc three"),
470 ]);
471
472 let mut stream = chain.stream(inputs).await.unwrap();
473 let mut tokens = Vec::new();
474 while let Some(item) = stream.next().await {
475 tokens.push(item.unwrap());
476 }
477 let text: String = tokens.iter().map(|t| t.token.as_str()).collect();
478 assert!(text.contains("refined answer"));
479 assert!(tokens.last().unwrap().is_final);
480 assert_eq!(invokes.load(Ordering::SeqCst), 2);
482 assert_eq!(streams.load(Ordering::SeqCst), 1);
483 }
484
485 #[tokio::test]
486 async fn test_refine_stream_empty_documents() {
487 let chain = RefineDocumentsChain::new(CountingLLM {
488 invokes: Arc::new(AtomicUsize::new(0)),
489 streams: Arc::new(AtomicUsize::new(0)),
490 });
491 let mut inputs = HashMap::new();
492 inputs.insert("input".to_string(), Value::String("q".to_string()));
493 inputs.insert("documents".to_string(), serde_json::json!([]));
494 let err = match chain.stream(inputs).await {
495 Ok(_) => panic!("expected an execution error"),
496 Err(e) => e,
497 };
498 assert!(matches!(err, ChainError::ExecutionError(_)));
499 }
500}