Skip to main content

langchain_rust/chain/conversational_retrieval_qa/
conversational_retrieval_qa.rs

1use 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};
18// _conversationalRetrievalQADefaultInputKey             = "question"
19// _conversationalRetrievalQADefaultSourceDocumentKey    = "source_documents"
20// 	_conversationalRetrievalQADefaultGeneratedQuestionKey = "generated_question"
21// )
22
23const 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,  //Default is `question`
34    pub(crate) output_key: String, //default is output
35}
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        // Execute the first `chain.invoke` and assert that it should succeed
292        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        // Optionally, if you want to print the successful result, you can do so like this:
300        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        // Execute the second `chain.invoke` and assert that it should succeed
308        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}