1use async_trait::async_trait;
8use futures_util::StreamExt;
9use lc_core::language_models::LLMResult;
10use lc_core::{BaseChatModel, Runnable};
11use lc_memory::{BaseMemory, ConversationBufferMemory};
12use lc_rag::retriever::RetrieverTrait;
13use lc_schema::{Message, MessageType};
14use lc_shared::document::Document;
15use serde_json::Value;
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::sync::Mutex;
19
20use crate::base::{BaseChain, ChainError, ChainResult, ChainStream, StreamToken};
21
22pub struct ConversationRetrievalChain<M: BaseChatModel> {
31 llm: M,
32 retriever: Arc<dyn RetrieverTrait>,
33 memory: Arc<Mutex<dyn BaseMemory>>,
34
35 system_prompt: Option<String>,
36 input_key: String,
37 output_key: String,
38 name: String,
39
40 k: usize,
41 verbose: bool,
42 return_source_documents: bool,
43 source_document_key: String,
44}
45
46impl<M: BaseChatModel + 'static> ConversationRetrievalChain<M> {
47 pub fn new(
48 llm: M,
49 retriever: Arc<dyn RetrieverTrait>,
50 memory: ConversationBufferMemory,
51 ) -> Self {
52 Self::from_memory(
57 llm,
58 retriever,
59 Arc::new(Mutex::new(
60 memory
61 .with_return_messages(true)
62 .with_input_key("query".to_string())
63 .with_output_key("result".to_string()),
64 )),
65 )
66 }
67
68 pub fn from_memory(
71 llm: M,
72 retriever: Arc<dyn RetrieverTrait>,
73 memory: Arc<Mutex<dyn BaseMemory>>,
74 ) -> Self {
75 Self {
76 llm,
77 retriever,
78 memory,
79 system_prompt: None,
80 input_key: "query".to_string(),
81 output_key: "result".to_string(),
82 name: "conversation_retrieval".to_string(),
83 k: 4,
84 verbose: false,
85 return_source_documents: false,
86 source_document_key: "source_documents".to_string(),
87 }
88 }
89
90 pub fn with_system_prompt(mut self, prompt: impl Into<String>) -> Self {
91 self.system_prompt = Some(prompt.into());
92 self
93 }
94
95 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 {
101 self.output_key = key.into();
102 self
103 }
104
105 pub fn with_name(mut self, name: impl Into<String>) -> Self {
106 self.name = name.into();
107 self
108 }
109
110 pub fn with_k(mut self, k: usize) -> Self {
111 self.k = k;
112 self
113 }
114
115 pub fn with_verbose(mut self, verbose: bool) -> Self {
116 self.verbose = verbose;
117 self
118 }
119
120 pub fn with_return_source_documents(mut self, return_source: bool) -> Self {
121 self.return_source_documents = return_source;
122 self
123 }
124
125 pub fn memory(&self) -> &Arc<Mutex<dyn BaseMemory>> {
126 &self.memory
127 }
128
129 pub async fn clear_memory(&self) -> Result<(), ChainError> {
130 let mut memory = self.memory.lock().await;
131 memory
132 .clear()
133 .await
134 .map_err(|e| ChainError::ExecutionError(format!("Failed to clear memory: {}", e)))?;
135 Ok(())
136 }
137
138 pub async fn query(&self, question: impl Into<String>) -> Result<String, ChainError> {
140 let inputs = HashMap::from([(self.input_key.clone(), Value::String(question.into()))]);
141 let result = self.invoke(inputs).await?;
142 result
143 .get(&self.output_key)
144 .and_then(|v| v.as_str())
145 .map(|s| s.to_string())
146 .ok_or_else(|| ChainError::OutputError("Missing output result".to_string()))
147 }
148
149 fn format_context(&self, documents: &[Document]) -> String {
150 documents
151 .iter()
152 .map(|doc| doc.content.clone())
153 .collect::<Vec<_>>()
154 .join("\n\n---\n\n")
155 }
156
157 pub fn build_messages(
159 &self,
160 history: &[Message],
161 context: &str,
162 question: &str,
163 ) -> Vec<Message> {
164 let mut messages = Vec::new();
165
166 if let Some(system) = &self.system_prompt {
167 messages.push(Message::system(system));
168 } else {
169 messages.push(Message::system(
170 "You are an AI assistant. Answer the user's question based on the conversation history and reference information."
171 ));
172 }
173
174 for msg in history {
175 messages.push(msg.clone());
176 }
177
178 let human_content = if context.is_empty() {
179 question.to_string()
180 } else {
181 format!(
182 "Reference information:\n{}\n\nQuestion: {}",
183 context, question
184 )
185 };
186 messages.push(Message::human(&human_content));
187
188 messages
189 }
190
191 fn format_history(&self, messages: &[Message]) -> String {
192 messages
193 .iter()
194 .map(|msg| {
195 let role = match msg.message_type {
196 MessageType::Human => "User",
197 MessageType::AI => "Assistant",
198 _ => "System",
199 };
200 format!("{}: {}", role, msg.content)
201 })
202 .collect::<Vec<_>>()
203 .join("\n")
204 }
205
206 async fn load_history(&self, question: &str) -> Result<Vec<Message>, ChainError> {
207 let memory = self.memory.lock().await;
208 let inputs = HashMap::from([(self.input_key.clone(), question.to_string())]);
209 let vars = memory
210 .load_memory_variables(&inputs)
211 .await
212 .map_err(|e| ChainError::ExecutionError(format!("Failed to load memory: {}", e)))?;
213 Ok(crate::base::variables_to_messages(&vars))
214 }
215
216 async fn save_context(&self, input: &str, output: &str) -> Result<(), ChainError> {
217 let mut memory = self.memory.lock().await;
218 let inputs = HashMap::from([(self.input_key.clone(), input.to_string())]);
219 let outputs = HashMap::from([(self.output_key.clone(), output.to_string())]);
220 memory
221 .save_context(&inputs, &outputs)
222 .await
223 .map_err(|e| ChainError::ExecutionError(format!("Failed to save context: {}", e)))?;
224 Ok(())
225 }
226}
227
228#[async_trait]
229impl<M: BaseChatModel + Send + Sync + 'static> BaseChain for ConversationRetrievalChain<M>
230where
231 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
232{
233 fn input_keys(&self) -> Vec<&str> {
234 vec![&self.input_key]
235 }
236
237 fn output_keys(&self) -> Vec<&str> {
238 if self.return_source_documents {
239 vec![&self.output_key, &self.source_document_key]
240 } else {
241 vec![&self.output_key]
242 }
243 }
244
245 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
246 self.validate_inputs(&inputs)?;
247
248 let question = inputs
249 .get(&self.input_key)
250 .and_then(|v| v.as_str())
251 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
252
253 if self.verbose {
254 println!("\n=== ConversationRetrievalChain Execution ===");
255 println!("Question: {}", question);
256 }
257
258 let history_messages = self.load_history(question).await?;
260 let history = self.format_history(&history_messages);
261
262 if self.verbose {
263 println!("History messages: {}", history_messages.len());
264 }
265
266 if self.verbose {
268 println!("\n--- Step 2: Retrieve relevant documents ---");
269 }
270
271 let documents = self
272 .retriever
273 .retrieve(question, self.k)
274 .await
275 .map_err(|e| ChainError::ExecutionError(format!("Retrieval failed: {}", e)))?;
276
277 if self.verbose {
278 println!("Retrieved {} documents", documents.len());
279 for (i, doc) in documents.iter().enumerate() {
280 let preview = if doc.content.len() > 100 {
281 &doc.content[..100]
282 } else {
283 &doc.content
284 };
285 println!("Document {}: {}", i + 1, preview);
286 }
287 }
288
289 if self.verbose {
291 println!("\n--- Step 3: Assemble Prompt ---");
292 }
293
294 let context = self.format_context(&documents);
295
296 if self.verbose {
297 println!("History length: {} characters", history.len());
298 println!("Context length: {} characters", context.len());
299 }
300
301 if self.verbose {
303 println!("\n--- Step 4: LLM generates answer ---");
304 }
305
306 let context_str = self.format_context(&documents);
307 let messages = self.build_messages(&history_messages, &context_str, question);
308
309 let response = self
310 .llm
311 .invoke(messages, None)
312 .await
313 .map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
314
315 let answer = response.content;
316
317 if self.verbose {
318 println!("Answer: {}", answer);
319 }
320
321 self.save_context(question, &answer).await?;
323
324 if self.verbose {
325 println!("=== ConversationRetrievalChain Complete ===\n");
326 }
327
328 let mut result = HashMap::new();
329 result.insert(self.output_key.clone(), Value::String(answer));
330
331 if self.return_source_documents {
332 let sources = crate::base::documents_to_values(&documents)?;
334 result.insert(self.source_document_key.clone(), Value::Array(sources));
335 }
336
337 Ok(result)
338 }
339
340 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
347 self.validate_inputs(&inputs)?;
348
349 let question = inputs
350 .get(&self.input_key)
351 .and_then(|v| v.as_str())
352 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
353
354 if self.verbose {
355 println!("\n=== ConversationRetrievalChain Stream ===");
356 println!("Question: {}", question);
357 }
358
359 let history_messages = self.load_history(question).await?;
361
362 let documents = self
364 .retriever
365 .retrieve(question, self.k)
366 .await
367 .map_err(|e| ChainError::ExecutionError(format!("Retrieval failed: {}", e)))?;
368
369 if self.verbose {
370 println!("Retrieved {} documents", documents.len());
371 }
372
373 let context = self.format_context(&documents);
375 let messages = self.build_messages(&history_messages, &context, question);
376
377 let llm_stream = self
379 .llm
380 .stream_chat(messages, None)
381 .await
382 .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
383
384 let memory = self.memory.clone();
385 let input_key = self.input_key.clone();
386 let output_key = self.output_key.clone();
387 let question_str = question.to_string();
388
389 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<String>();
392
393 let stream = llm_stream.map(move |result| match result {
394 Ok(token) => {
395 let _ = tx.send(token.clone());
396 Ok(StreamToken {
397 token,
398 is_final: false,
399 })
400 }
401 Err(e) => Err(ChainError::StreamError(format!(
402 "Stream token error: {}",
403 e
404 ))),
405 });
406
407 let finalizer_stream = async move {
408 let mut output = String::new();
409 let mut rx = rx;
410 while let Some(token) = rx.recv().await {
411 output.push_str(&token);
412 }
413
414 if !output.is_empty() {
416 let mut mem = memory.lock().await;
417 let ctx_inputs = HashMap::from([(input_key.clone(), question_str.clone())]);
418 let ctx_outputs = HashMap::from([(output_key.clone(), output)]);
419 if let Err(e) = mem.save_context(&ctx_inputs, &ctx_outputs).await {
420 log::error!("[ConversationRetrievalChain] failed to save context: {}", e);
421 }
422 }
423 };
424
425 let final_stream = stream.chain(futures_util::stream::once(async move {
426 finalizer_stream.await;
427 Ok(StreamToken {
428 token: String::new(),
429 is_final: true,
430 })
431 }));
432
433 Ok(Box::pin(final_stream))
434 }
435
436 fn name(&self) -> &str {
437 &self.name
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use async_trait::async_trait;
445 use futures_util::Stream;
446 use lc_core::language_models::LLMResult;
447 use lc_core::runnables::RunnableConfig;
448 use lc_core::{BaseLanguageModel, Runnable};
449 use lc_rag::retriever::RetrieverError;
450 use lc_shared::document::SearchResult;
451 use std::pin::Pin;
452
453 struct MockRetriever(Vec<Document>);
455
456 #[async_trait]
457 impl RetrieverTrait for MockRetriever {
458 async fn retrieve(&self, _query: &str, k: usize) -> Result<Vec<Document>, RetrieverError> {
459 Ok(self.0.iter().take(k).cloned().collect())
460 }
461 async fn retrieve_with_scores(
462 &self,
463 _query: &str,
464 _k: usize,
465 ) -> Result<Vec<SearchResult>, RetrieverError> {
466 Ok(Vec::new())
467 }
468 async fn add_documents(&self, _documents: Vec<Document>) -> Result<(), RetrieverError> {
469 Ok(())
470 }
471 }
472
473 #[derive(Debug)]
475 struct MockError(String);
476 impl std::fmt::Display for MockError {
477 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
478 write!(f, "{}", self.0)
479 }
480 }
481 impl std::error::Error for MockError {}
482
483 struct MockLLM;
484
485 #[async_trait]
486 impl Runnable<Vec<Message>, LLMResult> for MockLLM {
487 type Error = MockError;
488 async fn invoke(
489 &self,
490 _input: Vec<Message>,
491 _config: Option<RunnableConfig>,
492 ) -> Result<LLMResult, Self::Error> {
493 Ok(LLMResult {
494 content: "hello world".to_string(),
495 model: "mock".to_string(),
496 token_usage: None,
497 tool_calls: None,
498 thinking_content: None,
499 })
500 }
501 }
502
503 #[async_trait]
504 impl BaseLanguageModel<Vec<Message>, LLMResult> for MockLLM {
505 fn model_name(&self) -> &str {
506 "mock"
507 }
508 fn get_num_tokens(&self, t: &str) -> usize {
509 t.len()
510 }
511 fn with_temperature(self, _: f32) -> Self {
512 self
513 }
514 fn with_max_tokens(self, _: usize) -> Self {
515 self
516 }
517 }
518
519 #[async_trait]
520 impl BaseChatModel for MockLLM {
521 async fn chat(
522 &self,
523 _messages: Vec<Message>,
524 _config: Option<RunnableConfig>,
525 ) -> Result<LLMResult, Self::Error> {
526 Ok(LLMResult {
527 content: "hello world".to_string(),
528 model: "mock".to_string(),
529 token_usage: None,
530 tool_calls: None,
531 thinking_content: None,
532 })
533 }
534 async fn stream_chat(
535 &self,
536 _messages: Vec<Message>,
537 _config: Option<RunnableConfig>,
538 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error>
539 {
540 let tokens = [
541 Ok("hello".to_string()),
542 Ok(" ".to_string()),
543 Ok("world".to_string()),
544 ];
545 Ok(Box::pin(futures_util::stream::iter(tokens)))
546 }
547 }
548
549 fn doc(content: &str) -> Document {
550 Document::new(content.to_string())
551 }
552
553 #[tokio::test]
556 async fn test_conversation_retrieval_stream_saves_to_memory() {
557 let retriever: Arc<dyn RetrieverTrait> = Arc::new(MockRetriever(vec![doc("ctx")]));
558 let chain =
559 ConversationRetrievalChain::new(MockLLM, retriever, ConversationBufferMemory::new());
560 let inputs = HashMap::from([("query".to_string(), Value::String("q".to_string()))]);
561
562 let mut stream = chain.stream(inputs).await.unwrap();
563 let mut tokens = Vec::new();
564 while let Some(item) = stream.next().await {
565 tokens.push(item.unwrap());
566 }
567 let text: String = tokens.iter().map(|t| t.token.as_str()).collect();
568 assert_eq!(text, "hello world");
569 assert!(tokens.last().unwrap().is_final);
570
571 let memory = chain.memory().clone();
573 let mem = memory.lock().await;
574 let vars = mem.load_memory_variables(&HashMap::new()).await.unwrap();
575 let messages = crate::base::variables_to_messages(&vars);
576 assert!(
577 messages.iter().any(|m| m.content.contains("hello world")),
578 "memory should contain the streamed answer, got {:?}",
579 messages
580 );
581 }
582
583 #[tokio::test]
584 async fn test_conversation_retrieval_stream_missing_input() {
585 let retriever: Arc<dyn RetrieverTrait> = Arc::new(MockRetriever(vec![]));
586 let chain =
587 ConversationRetrievalChain::new(MockLLM, retriever, ConversationBufferMemory::new());
588 let err = match chain.stream(HashMap::new()).await {
589 Ok(_) => panic!("expected a missing-input error"),
590 Err(e) => e,
591 };
592 assert!(matches!(err, ChainError::MissingInput(_)));
593 }
594}