Tripagra
first commit
b33a861
Raw
History Blame Contribute Delete
7.5 kB
"""
RAG (Retrieval-Augmented Generation) system for the Smart Escalation API.
This module provides functionality to load help articles, generate embeddings,
build a FAISS vector store, and perform similarity search for customer questions.
"""
from typing import List, Dict, Any, Optional
from dataclasses import dataclass
import os
import numpy as np
from sentence_transformers import SentenceTransformer
import faiss
from src.chunking import chunk_text
@dataclass
class RetrievedChunk:
"""Represents a chunk retrieved from the vector store."""
content: str
score: float # Cosine similarity score
source: str # Help article filename
chunk_id: int
position: int
class RAGSystem:
"""
RAG system that loads help articles, generates embeddings, and performs similarity search.
The system uses sentence-transformers for embeddings and FAISS for efficient
similarity search over article chunks.
"""
def __init__(
self,
articles_dir: str = "data/articles",
embedding_model: str = "all-MiniLM-L6-v2",
chunk_size: int = 500,
chunk_overlap: int = 50
):
"""
Initialize the RAG system.
Args:
articles_dir: Directory containing help article markdown files
embedding_model: Name of the sentence-transformers model to use
chunk_size: Maximum size of each chunk in characters
chunk_overlap: Number of characters to overlap between chunks
"""
self.articles_dir = articles_dir
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
# Initialize embedding model
print(f"Loading embedding model: {embedding_model}...")
self.embedding_model = SentenceTransformer(embedding_model)
self.embedding_dim = self.embedding_model.get_embedding_dimension()
# Storage for chunks and metadata
self.chunks: List[Dict[str, Any]] = []
self.index: Optional[faiss.Index] = None
# Load articles and build index
self._load_articles()
self._build_index()
print(f"RAG system initialized with {len(self.chunks)} chunks")
def _load_articles(self) -> None:
"""
Load all markdown files from the articles directory and chunk them.
Reads all .md files from the articles directory, chunks each article,
and stores the chunks with metadata.
"""
if not os.path.exists(self.articles_dir):
raise FileNotFoundError(f"Articles directory not found: {self.articles_dir}")
article_files = [
f for f in os.listdir(self.articles_dir)
if f.endswith('.md') and f != '.gitkeep'
]
if not article_files:
raise ValueError(f"No markdown files found in {self.articles_dir}")
print(f"Loading {len(article_files)} articles from {self.articles_dir}...")
for filename in sorted(article_files):
filepath = os.path.join(self.articles_dir, filename)
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Chunk the article
article_chunks = chunk_text(
text=content,
source_article=filename,
chunk_size=self.chunk_size,
chunk_overlap=self.chunk_overlap
)
self.chunks.extend(article_chunks)
print(f" Loaded {filename}: {len(article_chunks)} chunks")
def _build_index(self) -> None:
"""
Generate embeddings for all chunks and build FAISS index.
Creates embeddings for each chunk using the sentence-transformers model
and builds an in-memory FAISS index for efficient similarity search.
"""
if not self.chunks:
raise ValueError("No chunks available to build index")
print("Generating embeddings for all chunks...")
# Extract chunk contents for embedding
chunk_texts = [chunk['content'] for chunk in self.chunks]
# Generate embeddings in batch
embeddings = self.embedding_model.encode(
chunk_texts,
show_progress_bar=True,
convert_to_numpy=True
)
# Normalize embeddings for cosine similarity
# FAISS inner product with normalized vectors = cosine similarity
faiss.normalize_L2(embeddings)
# Build FAISS index (using IndexFlatIP for inner product / cosine similarity)
self.index = faiss.IndexFlatIP(self.embedding_dim)
self.index.add(embeddings.astype('float32'))
print(f"FAISS index built with {self.index.ntotal} vectors")
def retrieve(self, question: str, top_k: int = 3) -> List[RetrievedChunk]:
"""
Retrieve the most relevant chunks for a given question.
Args:
question: The customer question to search for
top_k: Number of top chunks to retrieve (default: 3)
Returns:
List of RetrievedChunk objects ordered by descending relevance score
Example:
>>> rag = RAGSystem()
>>> chunks = rag.retrieve("How do I reset my password?", top_k=3)
>>> chunks[0].content
'To reset your password...'
>>> chunks[0].score
0.85
"""
if self.index is None:
raise RuntimeError("Index not built. Call _build_index() first.")
if not question or not question.strip():
return []
# Limit top_k to available chunks
actual_k = min(top_k, len(self.chunks))
if actual_k == 0:
return []
# Generate embedding for the question
question_embedding = self.embedding_model.encode(
[question],
convert_to_numpy=True
)
# Normalize for cosine similarity
faiss.normalize_L2(question_embedding)
# Search the index
scores, indices = self.index.search(
question_embedding.astype('float32'),
actual_k
)
# Build result list
results = []
for score, idx in zip(scores[0], indices[0]):
if idx < 0 or idx >= len(self.chunks):
continue
chunk = self.chunks[idx]
results.append(RetrievedChunk(
content=chunk['content'],
score=float(score),
source=chunk['source'],
chunk_id=chunk['chunk_id'],
position=chunk['position']
))
return results
def get_stats(self) -> Dict[str, Any]:
"""
Get statistics about the RAG system.
Returns:
Dictionary with system statistics including number of chunks,
articles, and index information
"""
sources = set(chunk['source'] for chunk in self.chunks)
return {
'total_chunks': len(self.chunks),
'total_articles': len(sources),
'articles': sorted(sources),
'embedding_dim': self.embedding_dim,
'index_size': self.index.ntotal if self.index else 0
}