darshit0503 commited on
Commit
fd26666
·
verified ·
1 Parent(s): 94e340a

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +14 -14
  2. app.py +238 -0
  3. requirements.txt +7 -0
README.md CHANGED
@@ -1,14 +1,14 @@
1
- ---
2
- title: RAG Chatbot
3
- emoji: 🔥
4
- colorFrom: yellow
5
- colorTo: gray
6
- sdk: gradio
7
- sdk_version: 5.46.0
8
- app_file: app.py
9
- pinned: false
10
- license: apache-2.0
11
- short_description: RAG Assignment
12
- ---
13
-
14
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
+ ---
2
+ title: RAG Chatbot
3
+ emoji: 🔥
4
+ colorFrom: yellow
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 5.46.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ short_description: RAG Assignment
12
+ ---
13
+
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import PyPDF2
3
+ from qdrant_client import QdrantClient
4
+ from dotenv import load_dotenv
5
+ from langchain_openai import AzureChatOpenAI, AzureOpenAIEmbeddings
6
+ from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
7
+ import streamlit as st
8
+ import hashlib
9
+
10
+ # Load environment variables from .env
11
+ load_dotenv(".env")
12
+
13
+ # Initialize Azure OpenAI (as in notebook)
14
+ llm = AzureChatOpenAI(
15
+ temperature=0,
16
+ api_key=os.getenv("AZURE_OPENAI_KEY"),
17
+ api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
18
+ azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
19
+ model=os.getenv("AZURE_OPENAI_MODEL_NAME") # Must match deployment name
20
+ )
21
+
22
+ # Qdrant configuration from environment
23
+ QDRANT_API_KEY = os.getenv('QDRANT_API_KEY')
24
+ QDRANT_URL = os.getenv('QDRANT_CLOUD_URL')
25
+
26
+ # Helper functions from notebook
27
+
28
+ def load_pdf_text(pdf_path):
29
+ text = ""
30
+ with open(pdf_path, 'rb') as f:
31
+ reader = PyPDF2.PdfReader(f)
32
+ for page in reader.pages:
33
+ page_text = page.extract_text() or ""
34
+ text += page_text + "\n"
35
+ return text
36
+
37
+ def split_text(text, chunk_size=800, chunk_overlap=150):
38
+ sentences = text.split('. ')
39
+ chunks, chunk = [], ''
40
+ for sentence in sentences:
41
+ next_piece = (sentence + '. ').strip()
42
+ if len(chunk) + len(next_piece) <= chunk_size:
43
+ chunk += (next_piece + ' ')
44
+ else:
45
+ if chunk:
46
+ chunks.append(chunk.strip())
47
+ # start new chunk with overlap
48
+ overlap = chunk[-chunk_overlap:] if chunk_overlap and len(chunk) > chunk_overlap else ''
49
+ chunk = (overlap + next_piece + ' ')
50
+ if chunk:
51
+ chunks.append(chunk.strip())
52
+ return chunks
53
+
54
+ # Azure embeddings helper
55
+
56
+ def _azure_base(url: str | None) -> str | None:
57
+ if not url:
58
+ return None
59
+ idx = url.find("/openai")
60
+ return url[:idx] if idx > 0 else url
61
+
62
+
63
+ def _init_azure_embedder():
64
+ return AzureOpenAIEmbeddings(
65
+ api_key=os.getenv("AZURE_OPENAI_EMBEDDING_API_KEY") or os.getenv("AZURE_OPENAI_KEY"),
66
+ azure_endpoint=_azure_base(os.getenv("AZURE_OPENAI_EMBEDDING_ENDPOINT") or os.getenv("AZURE_OPENAI_ENDPOINT")),
67
+ api_version=os.getenv("AZURE_OPENAI_EMBEDDING_API_VERSION") or os.getenv("AZURE_OPENAI_API_VERSION"),
68
+ model=os.getenv("AZURE_OPENAI_EMBEDDING_MODEL_NAME")
69
+ )
70
+
71
+ # Streamlit UI
72
+ st.title("Chatbot using PDF Documents")
73
+
74
+ # Sidebar: upload PDFs
75
+ with st.sidebar:
76
+ st.header("Upload PDFs")
77
+ uploaded_files = st.file_uploader(
78
+ "Upload one or more PDF files",
79
+ type=["pdf"],
80
+ accept_multiple_files=True
81
+ )
82
+
83
+ # Automatically process when files are uploaded or changed
84
+ files_sig = (lambda files: (None if not files else hashlib.sha1("|".join(sorted([
85
+ f"{uf.name}:{len((uf.getvalue() if hasattr(uf, 'getvalue') else uf.read()))}:{hashlib.sha1((uf.getvalue() if hasattr(uf, 'getvalue') else (uf.seek(0) or uf.read() or b''))).hexdigest()}" # type: ignore
86
+ for uf in files
87
+ ])).encode()).hexdigest()))(uploaded_files)
88
+ if uploaded_files:
89
+ if not QDRANT_URL or not QDRANT_API_KEY:
90
+ st.error("QDRANT_URL or QDRANT_API_KEY is missing in the .env file.")
91
+ elif files_sig != st.session_state.get('files_sig'):
92
+ with st.spinner("Processing PDFs and building index..."):
93
+ # Load and process uploaded PDF(s) with metadata and better chunking
94
+ pdf_chunks, pdf_meta = [], []
95
+ for uf in uploaded_files:
96
+ try:
97
+ uf.seek(0)
98
+ reader = PyPDF2.PdfReader(uf)
99
+ for page_idx, page in enumerate(reader.pages, start=1):
100
+ page_text = page.extract_text() or ""
101
+ if not page_text.strip():
102
+ continue
103
+ for ch in split_text(page_text, chunk_size=800, chunk_overlap=150):
104
+ pdf_chunks.append(ch)
105
+ pdf_meta.append({"source": uf.name, "page": page_idx})
106
+ except Exception as e:
107
+ st.error(f"Failed to read {uf.name}: {e}")
108
+
109
+ # Generate embeddings using Azure OpenAI Embeddings
110
+ embedder = _init_azure_embedder()
111
+ embeddings = embedder.embed_documents(pdf_chunks) if pdf_chunks else []
112
+
113
+ # Initialize Qdrant (always recreate to match embedding dimension)
114
+ client = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
115
+ collection_name = 'pdf-chatbot-collection'
116
+ dim = (len(embeddings[0]) if embeddings else 1536)
117
+ client.recreate_collection(
118
+ collection_name=collection_name,
119
+ vectors_config={"size": dim, "distance": "Cosine"}
120
+ )
121
+
122
+ # Index embeddings with metadata
123
+ points = [
124
+ {
125
+ "id": i,
126
+ "vector": emb,
127
+ "payload": {"text": chunk, **meta}
128
+ }
129
+ for i, (emb, chunk, meta) in enumerate(zip(embeddings, pdf_chunks, pdf_meta))
130
+ ]
131
+ if points:
132
+ client.upsert(collection_name=collection_name, points=points)
133
+
134
+ # Store in session for querying
135
+ st.session_state['qdrant_client'] = client
136
+ st.session_state['collection_name'] = collection_name
137
+ st.session_state['embedder'] = embedder
138
+ st.session_state['index_ready'] = True
139
+ st.session_state['files_sig'] = files_sig
140
+ st.success("Index built successfully. You can now ask questions.")
141
+
142
+ # Text cleaning utility for retrieved chunks
143
+
144
+ def clean_text(t: str) -> str:
145
+ if not t:
146
+ return ""
147
+ # Normalize whitespace
148
+ t = t.replace('\u00A0', ' ').replace('\t', ' ')
149
+ # Fix hyphenation across line breaks: "exam-\nple" -> "example"
150
+ t = t.replace('-\n', '')
151
+ # Collapse newlines and multiple spaces
152
+ t = '\n'.join(line.strip() for line in t.splitlines())
153
+ while ' ' in t:
154
+ t = t.replace(' ', ' ')
155
+ # Trim
156
+ return t.strip()
157
+
158
+ # Retrieval logic — synthesize a single structured answer with history-aware prompting
159
+
160
+ def retrieve_answer(query, top_k=4):
161
+ embedder = st.session_state.get('embedder')
162
+ client = st.session_state.get('qdrant_client')
163
+ collection_name = st.session_state.get('collection_name')
164
+
165
+ if not embedder or not client or not collection_name:
166
+ return "Index not initialized. Upload PDFs to build the index first."
167
+
168
+ query_emb = embedder.embed_query(query)
169
+ hits = client.search(collection_name=collection_name, query_vector=query_emb, limit=top_k)
170
+ contexts, citations = [], []
171
+ for h in hits:
172
+ payload = getattr(h, 'payload', {}) or {}
173
+ text = clean_text(payload.get('text', ''))
174
+ src = payload.get('source', 'document')
175
+ page = payload.get('page', None)
176
+ if text:
177
+ contexts.append(text)
178
+ citations.append(f"{src} (page {page})" if page else src)
179
+
180
+ context_block = "\n\n---\n\n".join(contexts[:top_k]) if contexts else ""
181
+
182
+ # Build system prompt to enforce structured, user-friendly answers (generic for any PDF)
183
+ system_prompt = (
184
+ "You are a reliable retrieval-augmented assistant that answers questions about any kind of PDF content "
185
+ "(technical, legal, scientific, financial, educational, etc.). Use ONLY the provided context snippets. "
186
+ "Do not speculate or invent facts. If the information is not present, reply exactly: 'Not found in documents.' "
187
+ "Return a clear, structured, user-friendly response with: a brief summary, bullet-point key facts, and a short conclusion. "
188
+ "Include short citations with source filename and page numbers when available. Be concise and neutral."
189
+ )
190
+
191
+ # Include brief chat history for continuity (last 3 exchanges)
192
+ history = st.session_state.get('messages', [])[-6:]
193
+ history_msgs = []
194
+ for m in history:
195
+ role = m.get('role')
196
+ content = m.get('content', '')
197
+ if role == 'user':
198
+ history_msgs.append(HumanMessage(content=content))
199
+ elif role == 'assistant':
200
+ history_msgs.append(AIMessage(content=content))
201
+
202
+ user_content = (
203
+ f"CONTEXT:\n{context_block}\n\n"
204
+ f"QUESTION: {query}\n\n"
205
+ "Format:\n# Answer\n\n- Bullet points of key facts\n\nConclusion\n\nCitations: list source and page numbers if available."
206
+ )
207
+
208
+ messages = [SystemMessage(content=system_prompt), *history_msgs, HumanMessage(content=user_content)]
209
+ result = llm.invoke(messages)
210
+ answer_text = getattr(result, 'content', str(result))
211
+
212
+ if citations:
213
+ answer_text += "\n\nSources: " + "; ".join(dict.fromkeys(citations))
214
+ return answer_text
215
+
216
+ # Simple chat-style UI (only shown after index is ready)
217
+ ready = st.session_state.get('index_ready')
218
+ if 'messages' not in st.session_state:
219
+ st.session_state['messages'] = []
220
+
221
+ if ready:
222
+ for msg in st.session_state['messages']:
223
+ with st.chat_message(msg['role']):
224
+ st.markdown(msg['content'])
225
+
226
+ user_input = st.chat_input("Ask a question about the uploaded PDFs")
227
+ if user_input:
228
+ st.session_state['messages'].append({"role": "user", "content": user_input})
229
+ with st.chat_message("user"):
230
+ st.markdown(user_input)
231
+
232
+ with st.chat_message("assistant"):
233
+ with st.spinner("Retrieving answer..."):
234
+ answer_text = retrieve_answer(user_input, top_k=4)
235
+ st.markdown(answer_text)
236
+ st.session_state['messages'].append({"role": "assistant", "content": answer_text})
237
+ else:
238
+ st.caption("Upload PDFs in the sidebar to start chatting.")
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ langchain
2
+ openai
3
+ python-dotenv
4
+ streamlit
5
+ langchain_openai
6
+ langchain_community
7
+ langchain_qdrant