Skip to main content

swiftide_query/answers/
simple.rs

1//! Generate an answer based on the current query
2use std::sync::Arc;
3use swiftide_core::{
4    Answer,
5    document::Document,
6    indexing::SimplePrompt,
7    prelude::*,
8    prompt::Prompt,
9    querying::{Query, states},
10};
11
12/// Generate an answer based on the current query
13///
14/// For most general purposes, this transformer should provide a sensible default. It takes either
15/// a transformation that has already been applied to the documents (in `Query::current`), or the
16/// documents themselves, and will then feed them as context with the _original_ question to an llm
17/// to generate an answer.
18///
19/// For the template context, the following variables are available:
20/// - **question**: The original question asked by the user
21/// - **original**: Alias for `question`
22/// - **current**: The current transformed query
23/// - **documents**: The documents to use as context
24///
25/// Optionally, a custom document template can be provided to render the documents in a specific
26/// way.
27#[derive(Debug, Clone, Builder)]
28pub struct Simple {
29    #[builder(setter(custom))]
30    client: Arc<dyn SimplePrompt>,
31    #[builder(default = "default_prompt()")]
32    prompt_template: Prompt,
33    #[builder(default, setter(into, strip_option))]
34    document_template: Option<Prompt>,
35}
36
37impl Simple {
38    pub fn builder() -> SimpleBuilder {
39        SimpleBuilder::default()
40    }
41
42    /// Builds a new simple answer generator from a client that implements [`SimplePrompt`].
43    ///
44    /// # Panics
45    ///
46    /// Panics if the build failed
47    pub fn from_client(client: impl SimplePrompt + 'static) -> Simple {
48        SimpleBuilder::default()
49            .client(client)
50            .to_owned()
51            .build()
52            .expect("Failed to build Simple")
53    }
54}
55
56impl SimpleBuilder {
57    pub fn client(&mut self, client: impl SimplePrompt + 'static) -> &mut Self {
58        self.client = Some(Arc::new(client) as Arc<dyn SimplePrompt>);
59        self
60    }
61}
62
63fn default_prompt() -> Prompt {
64    indoc::indoc! {"
65    Answer the following question based on the context provided:
66    {{ question }}
67
68    ## Constraints
69    * Do not include any information that is not in the provided context.
70    * If the question cannot be answered by the provided context, state that it cannot be answered.
71    * Answer the question completely and format it as markdown.
72
73    ## Context
74
75    ---
76    {{ documents }}
77    ---
78    "}
79    .into()
80}
81
82#[async_trait]
83impl Answer for Simple {
84    #[tracing::instrument(skip_all)]
85    async fn answer(&self, query: Query<states::Retrieved>) -> Result<Query<states::Answered>> {
86        let mut context = tera::Context::new();
87
88        context.insert("question", query.original());
89        context.insert("original", query.original());
90        context.insert("current", query.current());
91
92        // If there is a current transformation that is different from the original (transformed)
93        // query, use those as documents (i.e. a summary)
94        let documents = if !query.current().is_empty()
95            && query
96                .history()
97                .iter()
98                .rfind(|e| e.is_retrieval())
99                .is_some_and(|h| h.before() != query.current())
100        {
101            query.current().to_string()
102        } else if let Some(template) = &self.document_template {
103            let mut rendered_documents = Vec::new();
104            for document in query.documents() {
105                let rendered = template
106                    .clone()
107                    .with_context(tera::Context::from_serialize(document)?)
108                    .render()?;
109                rendered_documents.push(rendered);
110            }
111
112            rendered_documents.join("\n---\n")
113        } else {
114            query
115                .documents()
116                .iter()
117                .map(Document::content)
118                .collect::<Vec<_>>()
119                .join("\n---\n")
120        };
121        context.insert("documents", &documents);
122
123        let answer = self
124            .client
125            .prompt(self.prompt_template.clone().with_context(context))
126            .await?;
127
128        Ok(query.answered(answer))
129    }
130}
131
132#[cfg(test)]
133mod test {
134    use std::sync::Mutex;
135
136    use insta::assert_snapshot;
137    use swiftide_core::{MockSimplePrompt, indexing::Metadata, querying::TransformationEvent};
138
139    use super::*;
140
141    assert_default_prompt_snapshot!("question" => "What is love?", "documents" => "My context");
142
143    #[tokio::test]
144    async fn test_uses_current_if_present() {
145        let mut mock_client = MockSimplePrompt::new();
146
147        // I'll buy a beer for the first person who can think of a less insane way to do this
148        let received_prompt = Arc::new(Mutex::new(None));
149        let cloned = received_prompt.clone();
150        mock_client
151            .expect_prompt()
152            .withf(move |prompt| {
153                cloned.lock().unwrap().replace(prompt.clone());
154                true
155            })
156            .once()
157            .returning(|_| Ok(String::default()));
158
159        let documents = vec![
160            Document::new("First document", Some(Metadata::from(("some", "metadata")))),
161            Document::new(
162                "Second document",
163                Some(Metadata::from(("other", "metadata"))),
164            ),
165        ];
166        let query: Query<states::Retrieved> = Query::builder()
167            .original("original")
168            .current("A fictional generated summary")
169            .state(states::Retrieved)
170            .transformation_history(vec![TransformationEvent::Retrieved {
171                before: "abc".to_string(),
172                after: "abc".to_string(),
173                documents: documents.clone(),
174            }])
175            .documents(documents)
176            .build()
177            .unwrap();
178
179        let transformer = Simple::builder().client(mock_client).build().unwrap();
180
181        transformer.answer(query).await.unwrap();
182
183        let received_prompt = received_prompt.lock().unwrap().take().unwrap();
184        let rendered = received_prompt.render().unwrap();
185        assert_snapshot!(rendered);
186    }
187
188    #[tokio::test]
189    async fn test_custom_document_template() {
190        let mut mock_client = MockSimplePrompt::new();
191
192        // I'll buy a beer for the first person who can think of a less insane way to do this
193        let received_prompt = Arc::new(Mutex::new(None));
194        let cloned = received_prompt.clone();
195        mock_client
196            .expect_prompt()
197            .withf(move |prompt| {
198                cloned.lock().unwrap().replace(prompt.clone());
199                true
200            })
201            .once()
202            .returning(|_| Ok(String::default()));
203
204        let documents = vec![
205            Document::new("First document", Some(Metadata::from(("some", "metadata")))),
206            Document::new(
207                "Second document",
208                Some(Metadata::from(("other", "metadata"))),
209            ),
210        ];
211        let query: Query<states::Retrieved> = Query::builder()
212            .original("original")
213            .current(String::default())
214            .state(states::Retrieved)
215            .transformation_history(vec![TransformationEvent::Retrieved {
216                before: "abc".to_string(),
217                after: "abc".to_string(),
218                documents: documents.clone(),
219            }])
220            .documents(documents)
221            .build()
222            .unwrap();
223
224        let transformer = Simple::builder()
225            .client(mock_client)
226            .document_template(indoc::indoc! {"
227                {% for key, value in metadata -%}
228                    {{ key }}: {{ value }}
229                {% endfor -%}
230
231                {{ content }}"})
232            .build()
233            .unwrap();
234
235        transformer.answer(query).await.unwrap();
236
237        let received_prompt = received_prompt.lock().unwrap().take().unwrap();
238        let rendered = received_prompt.render().unwrap();
239        assert_snapshot!(rendered);
240    }
241}