File size: 7,394 Bytes
9b73f19
fc3f34d
5c31616
 
2be51ac
 
 
 
9b73f19
fc3f34d
 
990bd50
207e13d
fc3f34d
 
990bd50
fc3f34d
 
 
 
9b73f19
2be51ac
5c31616
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b73f19
5c31616
 
 
 
 
 
9b73f19
fc3f34d
2be51ac
5c31616
 
 
 
 
 
 
 
9b73f19
2be51ac
fc3f34d
2be51ac
5c31616
 
 
 
 
 
 
 
 
2be51ac
5c31616
c13b073
2be51ac
fc3f34d
5c31616
fc3f34d
5c31616
2be51ac
9b73f19
5c31616
 
6b008eb
9b73f19
5c31616
fc3f34d
9b73f19
fc3f34d
 
2be51ac
fc3f34d
 
2be51ac
9b73f19
fc3f34d
2be51ac
fc3f34d
 
 
 
 
 
e9be45f
 
40cec9a
e9be45f
 
5c31616
e9be45f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c31616
e9be45f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import os
import csv
import glob
from pypdf import PdfReader
from llama_index.core import VectorStoreIndex, Settings
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.schema import Document
from llama_index.embeddings.huggingface import HuggingFaceEmbedding

# -------------------------------------------------------------------------
# LlamaIndex configuration
# - Embeddings: sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 (Supports 50+ languages)
# - LLM: disabled β€” we manage the LLM ourselves in src/llm.py
# -------------------------------------------------------------------------
Settings.embed_model = HuggingFaceEmbedding(
    model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
)
Settings.llm = None

_index: VectorStoreIndex | None = None


# ── File loaders ─────────────────────────────────────────────────────────

def _load_csv(filepath: str) -> list[Document]:
    """Loads a CSV with 'Question' (and optionally 'Answer') columns."""
    docs = []
    try:
        with open(filepath, newline="", encoding="utf-8") as fh:
            reader = csv.DictReader(fh)
            if reader.fieldnames is None or "Question" not in reader.fieldnames:
                print(f"[RAG] Skipping {filepath}: no 'Question' column.")
                return docs
            has_answer = "Answer" in (reader.fieldnames or [])
            for row in reader:
                question = row.get("Question", "").strip()
                answer = row.get("Answer", "").strip() if has_answer else ""
                if question:
                    text = f"Q: {question}\nA: {answer}" if answer else f"Q: {question}"
                    docs.append(Document(text=text, metadata={"source": filepath}))
    except Exception as exc:
        print(f"[RAG] Error reading CSV {filepath}: {exc}")
    return docs


def _load_pdf(filepath: str) -> list[Document]:
    """Extracts text from each page of a PDF as separate documents."""
    docs = []
    try:
        reader = PdfReader(filepath)
        basename = os.path.basename(filepath)
        for i, page in enumerate(reader.pages):
            text = page.extract_text()
            if text and text.strip():
                docs.append(Document(
                    text=text.strip(),
                    metadata={"source": basename, "page": i + 1},
                ))
        print(f"[RAG] Loaded {len(docs)} pages from {basename}")
    except Exception as exc:
        print(f"[RAG] Error reading PDF {filepath}: {exc}")
    return docs


def _load_text(filepath: str) -> list[Document]:
    """Loads a plain text file as a single document."""
    docs = []
    try:
        with open(filepath, "r", encoding="utf-8") as fh:
            text = fh.read().strip()
            if text:
                docs.append(Document(text=text, metadata={"source": filepath}))
    except Exception as exc:
        print(f"[RAG] Error reading text file {filepath}: {exc}")
    return docs


# ── Vector store init ────────────────────────────────────────────────────

def init_vector_store(data_path: str = "data") -> None:
    """
    Scans the data directory and loads ALL supported files:
      - .csv  β†’ FAQ-style (Question/Answer columns)
      - .pdf  β†’ RBI guidelines, circulars, etc.
      - .txt  β†’ Plain text documents

    Builds an in-memory vector index from all loaded content.
    """
    global _index

    # Support both file path and directory path for backward compatibility
    if os.path.isfile(data_path):
        data_dir = os.path.dirname(data_path)
    else:
        data_dir = data_path

    if not os.path.isdir(data_dir):
        print(f"[RAG] Warning: data directory not found at '{data_dir}'. Vector store empty.")
        return

    documents: list[Document] = []

    # Auto-discover all supported files
    for pattern, loader in [("*.csv", _load_csv), ("*.pdf", _load_pdf), ("*.txt", _load_text)]:
        for filepath in sorted(glob.glob(os.path.join(data_dir, pattern))):
            loaded = loader(filepath)
            documents.extend(loaded)

    if not documents:
        print("[RAG] Warning: No documents loaded from any file.")
        return

    try:
        splitter = SentenceSplitter(chunk_size=512, chunk_overlap=50)
        nodes = splitter.get_nodes_from_documents(documents)
        _index = VectorStoreIndex(nodes)
        print(f"[RAG] Loaded {len(documents)} documents β†’ {len(nodes)} chunks into vector store.")
    except Exception as exc:
        print(f"[RAG] Error building vector index: {exc}")


# ── Retrieval ────────────────────────────────────────────────────────────

def retrieve_context(query: str, k: int = 5) -> str:
    """
    Returns the top-k most relevant passages for a query string.
    Falls back to an informative message if the index is not ready.
    """
    if not _index:
        return "Knowledge base not available."

    if not query or not query.strip():
        return "Knowledge base not available."

    try:
        retriever = _index.as_retriever(similarity_top_k=k)
        results = retriever.retrieve(query)
        if not results:
            return "No relevant information found in the knowledge base."
        return "\n\n---\n\n".join(node.get_content() for node in results)
    except Exception as exc:
        print(f"[RAG] Error during retrieval: {exc}")
        return "Knowledge base lookup failed."


def retrieve_context_multi(english_query: str, hindi_text: str, k: int = 15) -> str:
    """
    Searches using BOTH the English query and the original Hindi text,
    deduplicates, and returns the top-k most relevant passages.
    """
    if not _index:
        return "Knowledge base not available."

    seen_content: set[str] = set()
    all_results = []

    try:
        retriever = _index.as_retriever(similarity_top_k=k)

        # Search with English query
        if english_query and english_query.strip():
            for node in retriever.retrieve(english_query):
                content = node.get_content()
                if content not in seen_content:
                    seen_content.add(content)
                    all_results.append((node.get_score(), content))

        # Search with Hindi text (backup)
        if hindi_text and hindi_text.strip():
            for node in retriever.retrieve(hindi_text):
                content = node.get_content()
                if content not in seen_content:
                    seen_content.add(content)
                    all_results.append((node.get_score(), content))

        if not all_results:
            return "No relevant information found in the knowledge base."

        all_results.sort(key=lambda x: x[0], reverse=True)
        top_results = [content for _, content in all_results[:k]]
        print(f"[RAG] Retrieved {len(top_results)} unique passages from dual query")
        return "\n\n---\n\n".join(top_results)

    except Exception as exc:
        print(f"[RAG] Error during retrieval: {exc}")
        return "Knowledge base lookup failed."