Spaces:
Runtime error
Runtime error
| """ | |
| Naive RAG System β Basic Retrieval-Augmented Generation | |
| This is the simplest form of RAG (Retrieval-Augmented Generation). | |
| It adds one key improvement over the Base LLM: before answering, it | |
| searches a knowledge base to find relevant text chunks, then hands | |
| those chunks to the LLM as context. | |
| How it works: | |
| Step 1 β Embed the question: Convert the question into a vector (list of numbers) | |
| that captures its meaning. | |
| Step 2 β FAISS search: Find the chunks in the knowledge base whose vectors | |
| are closest to the question vector (semantic similarity). | |
| Step 3 β Generate: Give the top-k chunks + the question to the LLM and ask | |
| it to answer using ONLY what's in those chunks. | |
| Why is this "naive"? | |
| - It only uses one search method (vector similarity) | |
| - It searches with the raw question as-is (no rephrasing) | |
| - It uses all retrieved chunks equally (no reranking by relevance) | |
| Later systems fix these limitations one by one. | |
| This is the "entry-level RAG" baseline. Every advanced system should beat it. | |
| """ | |
| import asyncio | |
| import logging | |
| import time | |
| from langchain_groq import ChatGroq | |
| from langchain_core.prompts import ChatPromptTemplate | |
| from eval_framework.config import get_settings | |
| from eval_framework.systems.shared import SharedIndex | |
| from eval_framework.types import QAPair, SystemOutput | |
| logger = logging.getLogger(__name__) | |
| # Rough cost per output token for Groq-hosted Llama models | |
| _COST_PER_OUTPUT_TOKEN = 0.59 / 1_000_000 | |
| class NaiveRAGSystem: | |
| """ | |
| Standard RAG: embed question -> FAISS similarity search -> LLM generation. | |
| The starting point that every advanced technique builds on top of. | |
| Should score significantly better than BaseLLM on faithfulness and | |
| hallucination rate, since the LLM is grounded by real retrieved context. | |
| """ | |
| def __init__( | |
| self, | |
| index: SharedIndex, | |
| top_k: int = 3, | |
| model_name: str = "llama-3.3-70b-versatile", | |
| ): | |
| """ | |
| Args: | |
| index: The shared FAISS + BM25 index (built once, reused here). | |
| top_k: How many chunks to retrieve and show to the LLM. | |
| 3 is a good balance β enough context, not too noisy. | |
| model_name: Groq model for answer generation. | |
| """ | |
| self._index = index | |
| self.top_k = top_k | |
| self.model_name = model_name | |
| settings = get_settings() | |
| # LLM for generating answers β low temperature for factual consistency | |
| self._llm = ChatGroq( | |
| api_key=settings.groq_api_key, | |
| model_name=model_name, | |
| temperature=0.1, | |
| max_tokens=512, | |
| ) | |
| # Set up FAISS as a LangChain retriever. | |
| # "similarity" means: embed the query and find the nearest chunks by cosine distance. | |
| self._retriever = index.vectorstore.as_retriever( | |
| search_type="similarity", | |
| search_kwargs={"k": top_k}, | |
| ) | |
| # Prompt instructs the LLM to ONLY use the provided context. | |
| # This "grounding" instruction is what reduces hallucinations in RAG systems. | |
| self._prompt = ChatPromptTemplate.from_template( | |
| "You are a precise question-answering assistant. " | |
| "Answer the question using ONLY the information in the context below. " | |
| "If the context does not contain enough information, say: " | |
| "'The document does not contain enough information to answer this question.'\n\n" | |
| "Context:\n{context}\n\n" | |
| "Question: {question}\n\n" | |
| "Answer:" | |
| ) | |
| async def query(self, qa_pair: QAPair) -> SystemOutput: | |
| """ | |
| Retrieve relevant chunks with FAISS, then generate a grounded answer. | |
| Args: | |
| qa_pair: Contains the question to answer. | |
| Returns: | |
| SystemOutput with the answer, retrieved context, timing, and cost. | |
| """ | |
| start = time.time() | |
| # FAISS similarity search is a synchronous (blocking) operation. | |
| # We run it in a thread executor so it doesn't block the async event loop | |
| # while other questions are being processed concurrently. | |
| source_docs = await asyncio.get_event_loop().run_in_executor( | |
| None, lambda: self._retriever.invoke(qa_pair.question) | |
| ) | |
| # Join the retrieved chunks into one context block. | |
| # The "---" separator helps the LLM see where each chunk starts and ends. | |
| context = "\n\n---\n\n".join(doc.page_content for doc in source_docs) | |
| # Send context + question to the LLM for answer generation | |
| messages = await self._prompt.ainvoke({"context": context, "question": qa_pair.question}) | |
| response = await self._llm.ainvoke(messages) | |
| answer = response.content | |
| latency_ms = (time.time() - start) * 1000 | |
| # Store context on qa_pair so evaluators (faithfulness, etc.) can use it | |
| if context: | |
| qa_pair.context = context | |
| # Rough cost estimate: word count * 1.3 approximates token count | |
| output_tokens = len(answer.split()) * 1.3 | |
| estimated_cost = output_tokens * _COST_PER_OUTPUT_TOKEN | |
| logger.info(f"NaiveRAG answered in {latency_ms:.0f}ms | {len(source_docs)} chunks") | |
| return SystemOutput( | |
| answer=answer, | |
| latency_ms=latency_ms, | |
| cost_usd=estimated_cost, | |
| model=self.model_name, | |
| metadata={ | |
| "system": "naive_rag", | |
| "chunks_retrieved": len(source_docs), | |
| # Store first 100 chars of each chunk for debugging | |
| "source_snippets": [doc.page_content[:100] for doc in source_docs], | |
| }, | |
| ) | |