1use async_trait::async_trait;
7use futures_util::StreamExt;
8use lc_core::BaseChatModel;
9use lc_providers::{wrap_chat_model, ProviderError};
10use lc_rag::retriever::RetrieverTrait;
11use lc_schema::Message;
12use lc_shared::document::Document;
13use serde_json::Value;
14use std::collections::HashMap;
15use std::sync::Arc;
16
17use crate::base::{BaseChain, ChainError, ChainResult, ChainStream, StreamToken};
18use crate::BoxedChatModel;
19
20const DEFAULT_QA_PROMPT: &str = "Answer the question based on the following context. If the context does not contain relevant information, say 'I don't know'.
22
23Context:
24{context}
25
26Question: {question}
27
28Answer:";
29
30pub struct RetrievalQA {
37 llm: BoxedChatModel,
38 retriever: Arc<dyn RetrieverTrait>,
39
40 prompt_template: String,
41 input_key: String,
42 output_key: String,
43 name: String,
44
45 k: usize,
46 verbose: bool,
47
48 return_source_documents: bool,
49 source_document_key: String,
50}
51
52impl RetrievalQA {
53 pub fn new<L>(llm: L, retriever: Arc<dyn RetrieverTrait>) -> Self
55 where
56 L: BaseChatModel + Send + Sync + 'static,
57 L::Error: Into<ProviderError>,
58 {
59 Self {
60 llm: wrap_chat_model(llm),
61 retriever,
62 prompt_template: DEFAULT_QA_PROMPT.to_string(),
63 input_key: "query".to_string(),
64 output_key: "result".to_string(),
65 name: "retrieval_qa".to_string(),
66 k: 4,
67 verbose: false,
68 return_source_documents: false,
69 source_document_key: "source_documents".to_string(),
70 }
71 }
72
73 pub fn with_prompt_template(mut self, template: impl Into<String>) -> Self {
75 self.prompt_template = template.into();
76 self
77 }
78
79 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 {
87 self.output_key = key.into();
88 self
89 }
90
91 pub fn with_name(mut self, name: impl Into<String>) -> Self {
93 self.name = name.into();
94 self
95 }
96
97 pub fn with_k(mut self, k: usize) -> Self {
99 self.k = k;
100 self
101 }
102
103 pub fn with_verbose(mut self, verbose: bool) -> Self {
105 self.verbose = verbose;
106 self
107 }
108
109 pub fn with_return_source_documents(mut self, return_source: bool) -> Self {
111 self.return_source_documents = return_source;
112 self
113 }
114
115 pub fn with_source_document_key(mut self, key: impl Into<String>) -> Self {
117 self.source_document_key = key.into();
118 self
119 }
120
121 pub fn retriever(&self) -> &Arc<dyn RetrieverTrait> {
123 &self.retriever
124 }
125
126 pub fn k(&self) -> usize {
128 self.k
129 }
130
131 fn format_context(&self, documents: &[Document]) -> String {
132 documents
133 .iter()
134 .map(|doc| doc.content.clone())
135 .collect::<Vec<_>>()
136 .join("\n\n")
137 }
138
139 fn build_prompt(&self, context: &str, question: &str) -> String {
140 self.prompt_template
141 .replace("{context}", context)
142 .replace("{question}", question)
143 }
144
145 pub async fn query(&self, question: impl Into<String>) -> Result<String, ChainError> {
147 let inputs = HashMap::from([(self.input_key.clone(), Value::String(question.into()))]);
148
149 let result = self.invoke(inputs).await?;
150
151 result
152 .get(&self.output_key)
153 .and_then(|v| v.as_str())
154 .map(|s| s.to_string())
155 .ok_or_else(|| ChainError::OutputError("Missing output result".to_string()))
156 }
157
158 pub async fn query_with_sources(
160 &self,
161 question: impl Into<String>,
162 ) -> Result<(String, Vec<Document>), ChainError> {
163 let question = question.into();
167 self.run(&question).await
168 }
169
170 async fn run(&self, question: &str) -> Result<(String, Vec<Document>), ChainError> {
175 if self.verbose {
176 println!("\n=== RetrievalQA Execution ===");
177 println!("Question: {}", question);
178 println!("Retrieval count (k): {}", self.k);
179 println!("\n--- Step 1: Retrieve relevant documents ---");
180 }
181
182 let documents = self
183 .retriever
184 .retrieve(question, self.k)
185 .await
186 .map_err(|e| ChainError::ExecutionError(format!("Retrieval failed: {}", e)))?;
187
188 if self.verbose {
189 println!("Retrieved {} documents", documents.len());
190 for (i, doc) in documents.iter().enumerate() {
191 let preview: String = doc.content.chars().take(100).collect();
192 println!("Document {}: {}", i + 1, preview);
193 }
194 if documents.is_empty() {
195 println!("Warning: No relevant documents retrieved");
196 }
197 println!("\n--- Step 2: Assemble Prompt ---");
198 }
199
200 let context = self.format_context(&documents);
201 let prompt = self.build_prompt(&context, question);
202
203 if self.verbose {
204 println!("Context length: {} characters", context.len());
205 println!("Prompt length: {} characters", prompt.len());
206 println!("\n--- Step 3: LLM generates answer ---");
207 }
208
209 let messages = vec![Message::human(&prompt)];
210 let response = self
211 .llm
212 .invoke(messages, None)
213 .await
214 .map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
215
216 if self.verbose {
217 println!("Answer: {}", response.content);
218 println!("=== RetrievalQA Complete ===\n");
219 }
220
221 Ok((response.content, documents))
222 }
223}
224
225#[async_trait]
226impl BaseChain for RetrievalQA {
227 fn input_keys(&self) -> Vec<&str> {
228 vec![&self.input_key]
229 }
230
231 fn output_keys(&self) -> Vec<&str> {
232 if self.return_source_documents {
233 vec![&self.output_key, &self.source_document_key]
234 } else {
235 vec![&self.output_key]
236 }
237 }
238
239 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
240 self.validate_inputs(&inputs)?;
241
242 let question = inputs
243 .get(&self.input_key)
244 .and_then(|v| v.as_str())
245 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
246
247 let (answer, documents) = self.run(question).await?;
248
249 let mut result = HashMap::new();
250 result.insert(self.output_key.clone(), Value::String(answer));
251
252 if self.return_source_documents {
253 let sources = crate::base::documents_to_values(&documents)?;
255 result.insert(self.source_document_key.clone(), Value::Array(sources));
256 }
257
258 Ok(result)
259 }
260
261 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
267 self.validate_inputs(&inputs)?;
268
269 let question = inputs
270 .get(&self.input_key)
271 .and_then(|v| v.as_str())
272 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
273
274 if self.verbose {
275 println!("\n=== RetrievalQA Stream ===");
276 println!("Question: {}", question);
277 println!("Retrieval count (k): {}", self.k);
278 }
279
280 let documents = self
281 .retriever
282 .retrieve(question, self.k)
283 .await
284 .map_err(|e| ChainError::ExecutionError(format!("Retrieval failed: {}", e)))?;
285
286 if self.verbose {
287 println!("Retrieved {} documents", documents.len());
288 }
289
290 let context = self.format_context(&documents);
291 let prompt = self.build_prompt(&context, question);
292
293 let messages = vec![Message::human(&prompt)];
294 let llm_stream = self
295 .llm
296 .stream_chat(messages, None)
297 .await
298 .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
299
300 let stream = llm_stream.map(move |result| match result {
301 Ok(chunk) => Ok(StreamToken {
302 token: chunk.text,
303 is_final: false,
304 }),
305 Err(e) => Err(ChainError::StreamError(format!(
306 "Stream token error: {}",
307 e
308 ))),
309 });
310
311 let final_stream = stream.chain(futures_util::stream::once(async move {
312 Ok(StreamToken {
313 token: String::new(),
314 is_final: true,
315 })
316 }));
317
318 Ok(Box::pin(final_stream))
319 }
320
321 fn name(&self) -> &str {
322 &self.name
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use async_trait::async_trait;
330 use futures_util::Stream;
331 use lc_core::language_models::{LLMResult, StreamChunk};
332 use lc_core::runnables::RunnableConfig;
333 use lc_core::{BaseLanguageModel, Runnable};
334 use lc_rag::retriever::RetrieverError;
335 use lc_shared::document::SearchResult;
336 use std::pin::Pin;
337
338 struct MockRetriever(Vec<Document>);
340
341 #[async_trait]
342 impl RetrieverTrait for MockRetriever {
343 async fn retrieve(&self, _query: &str, k: usize) -> Result<Vec<Document>, RetrieverError> {
344 Ok(self.0.iter().take(k).cloned().collect())
345 }
346 async fn retrieve_with_scores(
347 &self,
348 _query: &str,
349 _k: usize,
350 ) -> Result<Vec<SearchResult>, RetrieverError> {
351 Ok(Vec::new())
352 }
353 async fn add_documents(&self, _documents: Vec<Document>) -> Result<(), RetrieverError> {
354 Ok(())
355 }
356 }
357
358 struct MockLLM;
360
361 #[async_trait]
362 impl Runnable<Vec<Message>, LLMResult> for MockLLM {
363 type Error = ProviderError;
364 async fn invoke(
365 &self,
366 _input: Vec<Message>,
367 _config: Option<RunnableConfig>,
368 ) -> Result<LLMResult, Self::Error> {
369 Ok(LLMResult {
370 content: "hello world".to_string(),
371 model: "mock".to_string(),
372 token_usage: None,
373 tool_calls: None,
374 thinking_content: None,
375 })
376 }
377 }
378
379 #[async_trait]
380 impl BaseLanguageModel<Vec<Message>, LLMResult> for MockLLM {
381 fn model_name(&self) -> &str {
382 "mock"
383 }
384 fn get_num_tokens(&self, t: &str) -> usize {
385 t.len()
386 }
387 fn with_temperature(self, _: f32) -> Self {
388 self
389 }
390 fn with_max_tokens(self, _: usize) -> Self {
391 self
392 }
393 }
394
395 #[async_trait]
396 impl BaseChatModel for MockLLM {
397 async fn chat(
398 &self,
399 _messages: Vec<Message>,
400 _config: Option<RunnableConfig>,
401 ) -> Result<LLMResult, Self::Error> {
402 Ok(LLMResult {
403 content: "hello world".to_string(),
404 model: "mock".to_string(),
405 token_usage: None,
406 tool_calls: None,
407 thinking_content: None,
408 })
409 }
410 async fn stream_chat(
411 &self,
412 _messages: Vec<Message>,
413 _config: Option<RunnableConfig>,
414 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk, Self::Error>> + Send>>, Self::Error>
415 {
416 let tokens = [
417 Ok(StreamChunk::new("hello")),
418 Ok(StreamChunk::new(" ")),
419 Ok(StreamChunk::new("world")),
420 ];
421 Ok(Box::pin(futures_util::stream::iter(tokens)))
422 }
423 }
424
425 fn doc(content: &str) -> Document {
426 Document::new(content.to_string())
427 }
428
429 #[tokio::test]
432 async fn test_retrieval_qa_stream_emits_tokens() {
433 let retriever: Arc<dyn RetrieverTrait> = Arc::new(MockRetriever(vec![doc("ctx")]));
434 let chain = RetrievalQA::new(MockLLM, retriever);
435 let inputs = HashMap::from([("query".to_string(), Value::String("q".to_string()))]);
436
437 let mut stream = chain.stream(inputs).await.unwrap();
438 let mut tokens = Vec::new();
439 while let Some(item) = stream.next().await {
440 tokens.push(item.unwrap());
441 }
442 let text: String = tokens.iter().map(|t| t.token.as_str()).collect();
443 assert_eq!(text, "hello world");
444 assert!(tokens.last().unwrap().is_final);
445 assert!(tokens.iter().filter(|t| !t.is_final).count() >= 2);
448 }
449
450 #[tokio::test]
451 async fn test_retrieval_qa_stream_missing_input() {
452 let retriever: Arc<dyn RetrieverTrait> = Arc::new(MockRetriever(vec![]));
453 let chain = RetrievalQA::new(MockLLM, retriever);
454 let err = match chain.stream(HashMap::new()).await {
455 Ok(_) => panic!("expected a missing-input error"),
456 Err(e) => e,
457 };
458 assert!(matches!(err, ChainError::MissingInput(_)));
459 }
460}