langchain_rust/chain/conversational_retrieval_qa/
conversational_retrieval_qa.rs1use futures::Stream;
2use futures_util::{pin_mut, StreamExt};
3use std::{collections::HashMap, pin::Pin, sync::Arc};
4
5use async_stream::stream;
6use async_trait::async_trait;
7use serde_json::{json, Value};
8use tokio::sync::Mutex;
9
10use crate::{
11 chain::{
12 Chain, ChainError, CondenseQuestionPromptBuilder, StuffQAPromptBuilder, DEFAULT_RESULT_KEY,
13 },
14 language_models::{GenerateResult, TokenUsage},
15 prompt::PromptArgs,
16 schemas::{BaseMemory, Message, Retriever, StreamData},
17};
18const CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_SOURCE_DOCUMENT_KEY: &str = "source_documents";
24const CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_GENERATED_QUESTION_KEY: &str = "generated_question";
25
26pub struct ConversationalRetrieverChain {
27 pub(crate) retriever: Box<dyn Retriever>,
28 pub memory: Arc<Mutex<dyn BaseMemory>>,
29 pub(crate) combine_documents_chain: Box<dyn Chain>,
30 pub(crate) condense_question_chain: Box<dyn Chain>,
31 pub(crate) rephrase_question: bool,
32 pub(crate) return_source_documents: bool,
33 pub(crate) input_key: String, pub(crate) output_key: String, }
36impl ConversationalRetrieverChain {
37 async fn get_question(
38 &self,
39 history: &[Message],
40 input: &str,
41 ) -> Result<(String, Option<TokenUsage>), ChainError> {
42 if history.is_empty() {
43 return Ok((input.to_string(), None));
44 }
45 let mut token_usage: Option<TokenUsage> = None;
46 let question = match self.rephrase_question {
47 true => {
48 let result = self
49 .condense_question_chain
50 .call(
51 CondenseQuestionPromptBuilder::new()
52 .question(input)
53 .chat_history(history)
54 .build(),
55 )
56 .await?;
57 if let Some(tokens) = result.tokens {
58 token_usage = Some(tokens);
59 };
60 result.generation
61 }
62 false => input.to_string(),
63 };
64
65 Ok((question, token_usage))
66 }
67}
68
69#[async_trait]
70impl Chain for ConversationalRetrieverChain {
71 async fn call(&self, input_variables: PromptArgs) -> Result<GenerateResult, ChainError> {
72 let output = self.execute(input_variables).await?;
73 let result: GenerateResult = serde_json::from_value(output[DEFAULT_RESULT_KEY].clone())?;
74 Ok(result)
75 }
76
77 async fn execute(
78 &self,
79 input_variables: PromptArgs,
80 ) -> Result<HashMap<String, Value>, ChainError> {
81 let mut token_usage: Option<TokenUsage> = None;
82 let input_variable = &input_variables
83 .get(&self.input_key)
84 .ok_or(ChainError::MissingInputVariable(self.input_key.clone()))?;
85
86 let human_message =
87 Message::new_human_message(crate::schemas::messages::value_to_text(input_variable));
88 let history = self.memory.lock().await.messages().await?;
89
90 let (question, token) = self.get_question(&history, &human_message.content).await?;
91 if let Some(token) = token {
92 token_usage = Some(token);
93 }
94
95 let documents = self
96 .retriever
97 .get_relevant_documents(&question)
98 .await
99 .map_err(|e| ChainError::RetrieverError(e.to_string()))?;
100
101 let mut output = self
102 .combine_documents_chain
103 .call(
104 StuffQAPromptBuilder::new()
105 .documents(&documents)
106 .question(question.clone())
107 .build(),
108 )
109 .await?;
110
111 match &output.tokens {
112 Some(tokens) => {
113 if let Some(mut token_usage) = token_usage {
114 token_usage.add(tokens);
115 output.tokens = Some(token_usage)
116 }
117 }
118 None => {}
119 }
120
121 {
122 let mut memory = self.memory.lock().await;
123 memory.add_message(human_message).await?;
124 memory
125 .add_message(Message::new_ai_message(&output.generation))
126 .await?;
127 }
128
129 let mut result = HashMap::new();
130 result.insert(self.output_key.clone(), json!(output.generation));
131
132 result.insert(DEFAULT_RESULT_KEY.to_string(), json!(output));
133
134 if self.return_source_documents {
135 result.insert(
136 CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_SOURCE_DOCUMENT_KEY.to_string(),
137 json!(documents),
138 );
139 }
140
141 if self.rephrase_question {
142 result.insert(
143 CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_GENERATED_QUESTION_KEY.to_string(),
144 json!(question),
145 );
146 }
147
148 Ok(result)
149 }
150
151 async fn stream(
152 &self,
153 input_variables: PromptArgs,
154 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamData, ChainError>> + Send>>, ChainError>
155 {
156 let input_variable = &input_variables
157 .get(&self.input_key)
158 .ok_or(ChainError::MissingInputVariable(self.input_key.clone()))?;
159
160 let human_message =
161 Message::new_human_message(crate::schemas::messages::value_to_text(input_variable));
162 let history = self.memory.lock().await.messages().await?;
163
164 let (question, _) = self.get_question(&history, &human_message.content).await?;
165
166 let documents = self
167 .retriever
168 .get_relevant_documents(&question)
169 .await
170 .map_err(|e| ChainError::RetrieverError(e.to_string()))?;
171
172 let stream = self
173 .combine_documents_chain
174 .stream(
175 StuffQAPromptBuilder::new()
176 .documents(&documents)
177 .question(question.clone())
178 .build(),
179 )
180 .await?;
181
182 let memory = self.memory.clone();
183 let complete_ai_message = Arc::new(Mutex::new(String::new()));
184 let complete_ai_message_clone = complete_ai_message.clone();
185 let output_stream = stream! {
186 pin_mut!(stream);
187 while let Some(result) = stream.next().await {
188 match result {
189 Ok(data) => {
190 let mut complete_ai_message_clone =
191 complete_ai_message_clone.lock().await;
192 complete_ai_message_clone.push_str(&data.content);
193
194 yield Ok(data);
195 },
196 Err(e) => {
197 yield Err(e);
198 }
199 }
200 }
201
202 let mut memory = memory.lock().await;
203 let ai_message = Message::new_ai_message(&complete_ai_message.lock().await);
204 for message in [human_message, ai_message] {
205 if let Err(e) = memory.add_message(message).await {
206 yield Err(e.into());
207 break;
208 }
209 }
210 };
211
212 Ok(Box::pin(output_stream))
213 }
214
215 fn get_input_keys(&self) -> Vec<String> {
216 vec![self.input_key.clone()]
217 }
218
219 fn get_output_keys(&self) -> Vec<String> {
220 let mut keys = Vec::new();
221 if self.return_source_documents {
222 keys.push(CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_SOURCE_DOCUMENT_KEY.to_string());
223 }
224
225 if self.rephrase_question {
226 keys.push(CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_GENERATED_QUESTION_KEY.to_string());
227 }
228
229 keys.push(self.output_key.clone());
230 keys.push(DEFAULT_RESULT_KEY.to_string());
231
232 keys
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use std::error::Error;
239
240 use crate::{
241 chain::ConversationalRetrieverChainBuilder,
242 llm::openai::{OpenAI, OpenAIModel},
243 memory::SimpleMemory,
244 prompt_args,
245 schemas::Document,
246 };
247
248 use super::*;
249
250 struct RetrieverTest {}
251 #[async_trait]
252 impl Retriever for RetrieverTest {
253 async fn get_relevant_documents(
254 &self,
255 _question: &str,
256 ) -> Result<Vec<Document>, Box<dyn Error>> {
257 Ok(vec![
258 Document::new(format!(
259 "\nQuestion: {}\nAnswer: {}\n",
260 "Which is the favorite text editor of luis", "Nvim"
261 )),
262 Document::new(format!(
263 "\nQuestion: {}\nAnswer: {}\n",
264 "How old is Luis", "24"
265 )),
266 Document::new(format!(
267 "\nQuestion: {}\nAnswer: {}\n",
268 "Where do luis live", "Peru"
269 )),
270 Document::new(format!(
271 "\nQuestion: {}\nAnswer: {}\n",
272 "Whts his favorite food", "Pan con chicharron"
273 )),
274 ])
275 }
276 }
277
278 #[tokio::test]
279 #[ignore]
280 async fn test_invoke_retriever_conversational() {
281 let llm = OpenAI::default().with_model(OpenAIModel::Gpt35.to_string());
282 let chain = ConversationalRetrieverChainBuilder::new()
283 .llm(llm)
284 .retriever(RetrieverTest {})
285 .memory(SimpleMemory::new().into())
286 .build()
287 .expect("Error building ConversationalChain");
288
289 let input_variables_first = prompt_args! {
290 "question" => "Hola",
291 };
292 let result_first = chain.invoke(input_variables_first).await;
294 assert!(
295 result_first.is_ok(),
296 "Error invoking LLMChain: {:?}",
297 result_first.err()
298 );
299
300 if let Ok(result) = result_first {
302 println!("Result: {:?}", result);
303 }
304
305 let input_variables_second = prompt_args! {
306 "question" => "Cual es la comida favorita de luis",
307 };
308 let result_second = chain.invoke(input_variables_second).await;
310 assert!(
311 result_second.is_ok(),
312 "Error invoking LLMChain: {:?}",
313 result_second.err()
314 );
315
316 if let Ok(result) = result_second {
317 println!("Result: {:?}", result);
318 }
319 }
320}