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 = {
89 let memory = self.memory.lock().await;
90 memory.messages()
91 };
92
93 let (question, token) = self.get_question(&history, &human_message.content).await?;
94 if let Some(token) = token {
95 token_usage = Some(token);
96 }
97
98 let documents = self
99 .retriever
100 .get_relevant_documents(&question)
101 .await
102 .map_err(|e| ChainError::RetrieverError(e.to_string()))?;
103
104 let mut output = self
105 .combine_documents_chain
106 .call(
107 StuffQAPromptBuilder::new()
108 .documents(&documents)
109 .question(question.clone())
110 .build(),
111 )
112 .await?;
113
114 match &output.tokens {
115 Some(tokens) => {
116 if let Some(mut token_usage) = token_usage {
117 token_usage.add(tokens);
118 output.tokens = Some(token_usage)
119 }
120 }
121 None => {}
122 }
123
124 {
125 let mut memory = self.memory.lock().await;
126 memory.add_message(human_message);
127 memory.add_message(Message::new_ai_message(&output.generation));
128 }
129
130 let mut result = HashMap::new();
131 result.insert(self.output_key.clone(), json!(output.generation));
132
133 result.insert(DEFAULT_RESULT_KEY.to_string(), json!(output));
134
135 if self.return_source_documents {
136 result.insert(
137 CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_SOURCE_DOCUMENT_KEY.to_string(),
138 json!(documents),
139 );
140 }
141
142 if self.rephrase_question {
143 result.insert(
144 CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_GENERATED_QUESTION_KEY.to_string(),
145 json!(question),
146 );
147 }
148
149 Ok(result)
150 }
151
152 async fn stream(
153 &self,
154 input_variables: PromptArgs,
155 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamData, ChainError>> + Send>>, ChainError>
156 {
157 let input_variable = &input_variables
158 .get(&self.input_key)
159 .ok_or(ChainError::MissingInputVariable(self.input_key.clone()))?;
160
161 let human_message =
162 Message::new_human_message(crate::schemas::messages::value_to_text(input_variable));
163 let history = {
164 let memory = self.memory.lock().await;
165 memory.messages()
166 };
167
168 let (question, _) = self.get_question(&history, &human_message.content).await?;
169
170 let documents = self
171 .retriever
172 .get_relevant_documents(&question)
173 .await
174 .map_err(|e| ChainError::RetrieverError(e.to_string()))?;
175
176 let stream = self
177 .combine_documents_chain
178 .stream(
179 StuffQAPromptBuilder::new()
180 .documents(&documents)
181 .question(question.clone())
182 .build(),
183 )
184 .await?;
185
186 let memory = self.memory.clone();
187 let complete_ai_message = Arc::new(Mutex::new(String::new()));
188 let complete_ai_message_clone = complete_ai_message.clone();
189 let output_stream = stream! {
190 pin_mut!(stream);
191 while let Some(result) = stream.next().await {
192 match result {
193 Ok(data) => {
194 let mut complete_ai_message_clone =
195 complete_ai_message_clone.lock().await;
196 complete_ai_message_clone.push_str(&data.content);
197
198 yield Ok(data);
199 },
200 Err(e) => {
201 yield Err(e);
202 }
203 }
204 }
205
206 let mut memory = memory.lock().await;
207 memory.add_message(human_message);
208 memory.add_message(Message::new_ai_message(&complete_ai_message.lock().await));
209 };
210
211 Ok(Box::pin(output_stream))
212 }
213
214 fn get_input_keys(&self) -> Vec<String> {
215 vec![self.input_key.clone()]
216 }
217
218 fn get_output_keys(&self) -> Vec<String> {
219 let mut keys = Vec::new();
220 if self.return_source_documents {
221 keys.push(CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_SOURCE_DOCUMENT_KEY.to_string());
222 }
223
224 if self.rephrase_question {
225 keys.push(CONVERSATIONAL_RETRIEVAL_QA_DEFAULT_GENERATED_QUESTION_KEY.to_string());
226 }
227
228 keys.push(self.output_key.clone());
229 keys.push(DEFAULT_RESULT_KEY.to_string());
230
231 keys
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use std::error::Error;
238
239 use crate::{
240 chain::ConversationalRetrieverChainBuilder,
241 llm::openai::{OpenAI, OpenAIModel},
242 memory::SimpleMemory,
243 prompt_args,
244 schemas::Document,
245 };
246
247 use super::*;
248
249 struct RetrieverTest {}
250 #[async_trait]
251 impl Retriever for RetrieverTest {
252 async fn get_relevant_documents(
253 &self,
254 _question: &str,
255 ) -> Result<Vec<Document>, Box<dyn Error>> {
256 Ok(vec![
257 Document::new(format!(
258 "\nQuestion: {}\nAnswer: {}\n",
259 "Which is the favorite text editor of luis", "Nvim"
260 )),
261 Document::new(format!(
262 "\nQuestion: {}\nAnswer: {}\n",
263 "How old is Luis", "24"
264 )),
265 Document::new(format!(
266 "\nQuestion: {}\nAnswer: {}\n",
267 "Where do luis live", "Peru"
268 )),
269 Document::new(format!(
270 "\nQuestion: {}\nAnswer: {}\n",
271 "Whts his favorite food", "Pan con chicharron"
272 )),
273 ])
274 }
275 }
276
277 #[tokio::test]
278 #[ignore]
279 async fn test_invoke_retriever_conversational() {
280 let llm = OpenAI::default().with_model(OpenAIModel::Gpt35.to_string());
281 let chain = ConversationalRetrieverChainBuilder::new()
282 .llm(llm)
283 .retriever(RetrieverTest {})
284 .memory(SimpleMemory::new().into())
285 .build()
286 .expect("Error building ConversationalChain");
287
288 let input_variables_first = prompt_args! {
289 "question" => "Hola",
290 };
291 let result_first = chain.invoke(input_variables_first).await;
293 assert!(
294 result_first.is_ok(),
295 "Error invoking LLMChain: {:?}",
296 result_first.err()
297 );
298
299 if let Ok(result) = result_first {
301 println!("Result: {:?}", result);
302 }
303
304 let input_variables_second = prompt_args! {
305 "question" => "Cual es la comida favorita de luis",
306 };
307 let result_second = chain.invoke(input_variables_second).await;
309 assert!(
310 result_second.is_ok(),
311 "Error invoking LLMChain: {:?}",
312 result_second.err()
313 );
314
315 if let Ok(result) = result_second {
316 println!("Result: {:?}", result);
317 }
318 }
319}