1use lc_core::language_models::BaseChatModel;
21use lc_embeddings::Embeddings;
22use lc_providers::ProviderError;
23use lc_schema::Message;
24use lc_vector_stores::{Document, VectorStore, VectorStoreError};
25
26use crate::retriever::{RetrieverError, RetrieverTrait, SimilarityRetriever};
27
28use std::sync::Arc;
29
30pub struct RAGPipeline {
39 llm: Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>,
40 retriever: Arc<dyn RetrieverTrait>,
41 retrieve_k: usize,
43 system_prompt: String,
45}
46
47impl std::fmt::Debug for RAGPipeline {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("RAGPipeline")
50 .field("model_name", &self.llm.model_name())
51 .field("retrieve_k", &self.retrieve_k)
52 .field("system_prompt", &self.system_prompt)
53 .finish()
54 }
55}
56
57impl RAGPipeline {
58 pub async fn index_documents(&self, documents: Vec<Document>) -> Result<(), RetrieverError> {
63 self.retriever.add_documents(documents).await
64 }
65
66 pub async fn query(&self, question: &str) -> Result<String, RetrieverError> {
72 let result = self.query_with_sources(question).await?;
73 Ok(result.answer)
74 }
75
76 pub async fn query_with_sources(
80 &self,
81 question: &str,
82 ) -> Result<RAGQueryResult, RetrieverError> {
83 let search_results = self
85 .retriever
86 .retrieve_with_scores(question, self.retrieve_k)
87 .await?;
88
89 let sources: Vec<Document> = search_results.iter().map(|r| r.document.clone()).collect();
90
91 let context = if sources.is_empty() {
93 "No relevant documents found.".to_string()
94 } else {
95 sources
96 .iter()
97 .enumerate()
98 .map(|(i, doc)| format!("[{}] {}", i + 1, doc.page_content()))
99 .collect::<Vec<_>>()
100 .join("\n\n")
101 };
102
103 let messages = vec![
105 Message::system(format!(
106 "{}\n\nUse the following context to answer the question. If the context doesn't contain the answer, say so.",
107 self.system_prompt
108 )),
109 Message::human(format!("Context:\n{}\n\nQuestion: {}", context, question)),
110 ];
111
112 let llm_result = self
113 .llm
114 .chat(messages, None)
115 .await
116 .map_err(|e| RetrieverError::EmbeddingError(format!("LLM call failed: {}", e)))?;
117
118 Ok(RAGQueryResult {
119 answer: llm_result.content,
120 sources,
121 })
122 }
123}
124
125#[derive(Debug, Clone)]
127pub struct RAGQueryResult {
128 pub answer: String,
130 pub sources: Vec<Document>,
132}
133
134pub struct RAGPipelineBuilder {
150 llm: Option<Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>>,
151 embeddings: Option<Arc<dyn Embeddings + Send + Sync>>,
152 vector_store: Option<Arc<dyn VectorStore + Send + Sync>>,
153 retriever: Option<Arc<dyn RetrieverTrait>>,
156 retrieve_k: usize,
157 system_prompt: Option<String>,
158}
159
160impl RAGPipelineBuilder {
161 pub fn new() -> Self {
163 Self {
164 llm: None,
165 embeddings: None,
166 vector_store: None,
167 retriever: None,
168 retrieve_k: 4,
169 system_prompt: None,
170 }
171 }
172
173 pub fn llm<L>(mut self, llm: L) -> Self
175 where
176 L: BaseChatModel + Send + Sync + 'static,
177 L::Error: Into<ProviderError>,
178 {
179 self.llm = Some(lc_providers::wrap_chat_model(llm));
180 self
181 }
182
183 pub fn llm_from_arc(
185 mut self,
186 llm: Arc<dyn BaseChatModel<Error = ProviderError> + Send + Sync>,
187 ) -> Self {
188 self.llm = Some(llm);
189 self
190 }
191
192 pub fn llm_client(mut self, client: lc_providers::LLMClient) -> Self {
194 let provider_arc = client.into_inner();
195 self.llm = Some(provider_arc);
196 self
197 }
198
199 pub fn embeddings<E: Embeddings + Send + Sync + 'static>(mut self, embeddings: E) -> Self {
201 self.embeddings = Some(Arc::new(embeddings));
202 self
203 }
204
205 pub fn vector_store<V: VectorStore + Send + Sync + 'static>(mut self, store: V) -> Self {
207 self.vector_store = Some(Arc::new(store));
208 self
209 }
210
211 pub fn retriever<R>(mut self, retriever: R) -> Self
217 where
218 R: RetrieverTrait + Send + Sync + 'static,
219 {
220 self.retriever = Some(Arc::new(retriever));
221 self
222 }
223
224 pub fn retriever_from_arc(mut self, retriever: Arc<dyn RetrieverTrait>) -> Self {
226 self.retriever = Some(retriever);
227 self
228 }
229
230 pub fn retrieve_k(mut self, k: usize) -> Self {
232 self.retrieve_k = k;
233 self
234 }
235
236 pub fn system(mut self, prompt: impl Into<String>) -> Self {
238 self.system_prompt = Some(prompt.into());
239 self
240 }
241
242 pub fn build(self) -> Result<RAGPipeline, RetrieverError> {
248 let llm = self.llm.ok_or_else(|| {
249 RetrieverError::EmbeddingError(
250 "RAGPipelineBuilder: LLM is required. Call .llm() first.".into(),
251 )
252 })?;
253
254 let retriever = match self.retriever {
257 Some(r) => r,
258 None => {
259 let embeddings = self.embeddings.ok_or_else(|| {
260 RetrieverError::EmbeddingError(
261 "RAGPipelineBuilder: Embeddings is required (or use .retriever()). Call .embeddings() first."
262 .into(),
263 )
264 })?;
265
266 let vector_store = self.vector_store.ok_or_else(|| {
267 RetrieverError::StoreError(VectorStoreError::StorageError(
268 "RAGPipelineBuilder: VectorStore is required (or use .retriever()). Call .vector_store() first."
269 .into(),
270 ))
271 })?;
272
273 Arc::new(SimilarityRetriever::new(vector_store, embeddings))
274 }
275 };
276
277 Ok(RAGPipeline {
278 llm,
279 retriever,
280 retrieve_k: self.retrieve_k,
281 system_prompt: self.system_prompt.unwrap_or_else(|| {
282 "You are a helpful assistant that answers questions based on the provided context.".to_string()
283 }),
284 })
285 }
286}
287
288impl Default for RAGPipelineBuilder {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 use lc_embeddings::MockEmbeddings;
298 use lc_providers::{OpenAIChat, OpenAIConfig};
299 use lc_vector_stores::InMemoryVectorStore;
300
301 #[test]
302 fn test_builder_missing_llm() {
303 let result = RAGPipelineBuilder::new()
304 .embeddings(MockEmbeddings::new(3))
305 .vector_store(InMemoryVectorStore::new())
306 .build();
307
308 assert!(result.is_err());
309 assert!(result.unwrap_err().to_string().contains("LLM is required"));
310 }
311
312 #[test]
313 fn test_builder_missing_embeddings() {
314 let config = OpenAIConfig::new("test_key").with_base_url("http://localhost:8080/v1");
315 let result = RAGPipelineBuilder::new()
316 .llm(OpenAIChat::new(config))
317 .vector_store(InMemoryVectorStore::new())
318 .build();
319
320 assert!(result.is_err());
321 assert!(result
322 .unwrap_err()
323 .to_string()
324 .contains("Embeddings is required"));
325 }
326
327 #[test]
328 fn test_builder_missing_vector_store() {
329 let config = OpenAIConfig::new("test_key").with_base_url("http://localhost:8080/v1");
330 let result = RAGPipelineBuilder::new()
331 .llm(OpenAIChat::new(config))
332 .embeddings(MockEmbeddings::new(3))
333 .build();
334
335 assert!(result.is_err());
336 assert!(result
337 .unwrap_err()
338 .to_string()
339 .contains("VectorStore is required"));
340 }
341
342 #[test]
343 fn test_builder_success() {
344 let config = OpenAIConfig::new("test_key").with_base_url("http://localhost:8080/v1");
345 let result = RAGPipelineBuilder::new()
346 .llm(OpenAIChat::new(config))
347 .embeddings(MockEmbeddings::new(3))
348 .vector_store(InMemoryVectorStore::new())
349 .system("You are a test assistant.")
350 .retrieve_k(5)
351 .build();
352
353 assert!(result.is_ok());
354 let pipeline = result.unwrap();
355 assert_eq!(pipeline.retrieve_k, 5);
356 assert_eq!(pipeline.system_prompt, "You are a test assistant.");
357 }
358
359 #[test]
360 fn test_builder_default() {
361 let builder = RAGPipelineBuilder::default();
362 assert_eq!(builder.retrieve_k, 4);
363 assert!(builder.llm.is_none());
364 }
365
366 #[tokio::test]
369 async fn test_builder_with_custom_retriever() {
370 use crate::bm25::BM25Retriever;
371
372 let config = OpenAIConfig::new("test_key").with_base_url("http://localhost:8080/v1");
373 let rag = RAGPipelineBuilder::new()
374 .llm(OpenAIChat::new(config))
375 .retriever(BM25Retriever::new())
376 .build()
377 .expect("build should succeed");
378
379 rag.index_documents(vec![Document::new("Rust is a systems language")])
380 .await
381 .expect("index_documents should delegate to BM25 retriever successfully");
382 }
383}