"""RAGWithUsage — production base class that adds hybrid search and usage tracking. Kept separate from evaluation/ so that production code does not depend on the evaluation package. Notebooks and evaluation modules import from here too. """ from rag.rag_helper import RAGBase from search import rrf, text_search, vector_search def calc_total_price(usages): """Sum cost across a list of usage objects (prompt + completion tokens at $0.10/M).""" total = 0.0 for usage in usages: total += (usage.prompt_tokens + usage.completion_tokens) * 0.1 / 1_000_000 return total class RAGWithUsage(RAGBase): def __init__(self, *args, **kwargs): self.text_index = kwargs.pop("text_index", None) self.vector_index = kwargs.pop("vector_index", None) self.embedder = kwargs.pop("embedder", None) kwargs.setdefault("index", None) super().__init__(*args, **kwargs) if self.vector_index is not None and self.embedder is not None: self.index = self.vector_index self.usages = [] self.last_usage = None def reset_usage(self): self.usages = [] self.last_usage = None def search(self, query, num_results=10): if ( self.text_index is not None and self.vector_index is not None and self.embedder is not None ): text_results = text_search(query, self.text_index, num_results=10) vector_results = vector_search( query, self.embedder, self.vector_index, num_results=10, ) return rrf( [text_results, vector_results], k=60, num_results=num_results, ) if self.text_index is not None: return text_search(query, self.text_index, num_results=num_results) if self.vector_index is not None and self.embedder is not None: return vector_search( query, self.embedder, self.vector_index, num_results=num_results, ) def llm(self, prompt): response = self._call_llm(prompt) self.last_usage = response.usage self.usages.append(response.usage) return response.choices[0].message.content def total_cost(self): return calc_total_price(self.usages)