abhivsh commited on
Commit
05cfa61
·
verified ·
1 Parent(s): 58f24a8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +451 -0
app.py CHANGED
@@ -0,0 +1,451 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import fitz
3
+ import zipfile
4
+ import requests
5
+ import gradio as gr
6
+ from dotenv import load_dotenv
7
+
8
+ # LangChain & Vector DB imports
9
+ from langchain_huggingface import HuggingFaceEmbeddings
10
+ from langchain_chroma import Chroma
11
+ from langchain_groq import ChatGroq
12
+ from langchain_core.documents import Document
13
+ from langchain_core.prompts import (
14
+ PromptTemplate,
15
+ ChatPromptTemplate,
16
+ SystemMessagePromptTemplate,
17
+ HumanMessagePromptTemplate,
18
+ MessagesPlaceholder,
19
+ )
20
+ from langchain_core.runnables.history import RunnableWithMessageHistory
21
+ from langchain_core.runnables import RunnablePassthrough
22
+ from langchain_core.output_parsers import StrOutputParser
23
+ from langchain_community.chat_message_histories import ChatMessageHistory
24
+ from langchain_core.messages import HumanMessage, AIMessage
25
+
26
+ load_dotenv()
27
+
28
+ # --- Configurations & Secrets ---
29
+ HF_TOKEN = os.getenv('HF_token')
30
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
31
+ SOURCE_URL = os.getenv('URL')
32
+ PERSIST_DIR = './chroma_db/'
33
+ GROQ_MODEL = "llama-3.3-70b-versatile"
34
+ DESTINATION_FOLDER = "Model_TS"
35
+
36
+ # --- Initialization Downloading ---
37
+ def download_and_extract_zip(url, destination_folder):
38
+ """Downloads a zip file from a URL and extracts its contents to the specified destination folder."""
39
+ zip_file_path = "temp.zip"
40
+
41
+ try:
42
+ # Send an HTTP GET request to the OneDrive link to download the file
43
+ response = requests.get(url)
44
+ # Check if the request was successful (status code 200)
45
+ response.raise_for_status()
46
+ # Save the zip file to a temporary location
47
+ with open(zip_file_path, "wb") as f:
48
+ f.write(response.content)
49
+ # Create the destination folder if it doesn't exist
50
+ os.makedirs(destination_folder, exist_ok=True)
51
+ # Extract the contents of the zip file
52
+ with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
53
+ zip_ref.extractall(destination_folder)
54
+ print(f"Zip file downloaded and extracted to: {destination_folder}")
55
+
56
+ except requests.exceptions.RequestException as e:
57
+ print(f"Error downloading file: {e}")
58
+
59
+ finally:
60
+ # Remove the temporary zip file
61
+ if os.path.exists(zip_file_path):
62
+ os.remove(zip_file_path)
63
+
64
+
65
+ # Splitting, Initialize Embeddings and VectorDB Storage
66
+ download_and_extract_zip(SOURCE_URL, DESTINATION_FOLDER)
67
+
68
+ def gen_splits(folder_name):
69
+ file_paths = os.listdir(folder_name)
70
+ new_file_paths = [os.path.join(os.getcwd(), folder_name, file) for file in file_paths]
71
+
72
+ splits = []
73
+ for file_path in new_file_paths:
74
+ if not file_path.lower().endswith(".pdf"):
75
+ continue
76
+
77
+ # Open document using fitz
78
+ doc = fitz.open(file_path)
79
+ file_name = os.path.basename(file_path)
80
+
81
+ for page_num in range(len(doc)):
82
+ page = doc.load_page(page_num)
83
+ text = page.get_text("text") # "text" maintains logical flow; "blocks" is better for tables
84
+
85
+ # Creating a LangChain Document object for each page
86
+ # This replaces the need for RecursiveCharacterTextSplitter
87
+ page_doc = Document(
88
+ page_content=text,
89
+ metadata={
90
+ "source": file_name,
91
+ "page": page_num + 1, # 1-indexed for user readability
92
+ "total_pages": len(doc),
93
+ "format": "PDF",
94
+ "extraction_method": "PyMuPDF"
95
+ }
96
+ )
97
+ splits.append(page_doc)
98
+
99
+ doc.close()
100
+
101
+ return splits
102
+
103
+ splits = gen_splits(DESTINATION_FOLDER)
104
+ embedding_func = HuggingFaceEmbeddings(model_name='all-MiniLM-L6-v2')
105
+
106
+ def vectordb_from_splits(splits):
107
+ vectordb = Chroma.from_documents(documents=splits, persist_directory=PERSIST_DIR, embedding=embedding_func)
108
+ return vectordb
109
+
110
+ vectordb = vectordb_from_splits(splits)
111
+
112
+
113
+
114
+ # RAG Chain
115
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY") # set in HF Spaces → Settings → Secrets
116
+
117
+ # ── Model options on Groq free tier (swap as needed) ──────────────────────────
118
+ # "llama-3.3-70b-versatile" ← RECOMMENDED: best reasoning, table fidelity
119
+ # "llama3-8b-8192" ← fallback if hitting TPM limits
120
+ # "qwen-qwq-32b" ← strong reasoning, good for clause referencing
121
+ # "deepseek-r1-distill-llama-70b" ← chain-of-thought style; verbose but thorough
122
+ GROQ_MODEL = "llama-3.3-70b-versatile"
123
+
124
+ # ── Session store ──────────────────────────────────────────────────────────────
125
+ session_store: dict = {}
126
+
127
+ def get_session_history(session_id: str) -> ChatMessageHistory:
128
+ if session_id not in session_store:
129
+ session_store[session_id] = ChatMessageHistory()
130
+ return session_store[session_id]
131
+
132
+
133
+ def get_file(source_documents):
134
+ references, files_in_order = [], []
135
+ seen_refs, seen_files = set(), set()
136
+ for doc in source_documents:
137
+ source = os.path.basename(doc.metadata.get("source", "unknown"))
138
+ page = doc.metadata.get("page", 0) + 1
139
+ ref = f"Page-{page} of {source}"
140
+ if ref not in seen_refs:
141
+ references.append(ref)
142
+ seen_refs.add(ref)
143
+ if source not in seen_files:
144
+ files_in_order.append(source)
145
+ seen_files.add(source)
146
+ return references, files_in_order
147
+
148
+
149
+ def build_chain(vectordb: Chroma):
150
+ system_instruction = (
151
+ "You are an expert **Electrical Engineer AI Assistant**, specialized in power systems "
152
+ "and substation design (AIS/GIS up to 765kV), providing insights strictly from the provided context.\n\n"
153
+ "**Formatting Guidelines:**\n"
154
+ "1. Organize using **bullet points or numbered lists** where appropriate.\n"
155
+ "2. **Bold** key technical terms, parameters, and essential facts.\n"
156
+ "3. Use **technical language** consistent with IEC/IEEE/POWERGRID standards.\n"
157
+ "4. For multi-step explanations, use **sub-headings** (e.g., `## Sub-section`).\n"
158
+ "5. **Always include clause references (e.g., Clause XX.XX) for every piece of information.**\n"
159
+ "6. **CRITICAL: If context contains a table, reproduce it EXACTLY — preserve all rows, "
160
+ "columns, headers, and alignment. Never paraphrase table data.**\n\n"
161
+ "**Context Prioritization:**\n"
162
+ "1. Prioritize documents directly related to the queried equipment type.\n"
163
+ "2. 'Specific Requirements' clauses **supersede** all other documents — reflect modified clauses first.\n"
164
+ "3. If context is insufficient: 'The available documents do not contain information regarding [detail].'\n"
165
+ "4. **Do not invent information** outside the provided context."
166
+ )
167
+
168
+ prompt = ChatPromptTemplate.from_messages([
169
+ SystemMessagePromptTemplate.from_template(system_instruction),
170
+ MessagesPlaceholder(variable_name="chat_history"),
171
+ HumanMessagePromptTemplate.from_template(
172
+ "Context:\n{context}\n\nQuestion:\n{question}"
173
+ ),
174
+ ])
175
+
176
+ # ── Groq LLM ───────────────────────────────────────────────────────────────
177
+ llm = ChatGroq(
178
+ model=GROQ_MODEL,
179
+ temperature=0.1,
180
+ max_tokens=2048,
181
+ api_key=GROQ_API_KEY,
182
+ )
183
+
184
+ # ── Retriever ──────────────────────────────────────────────────────────────
185
+ retriever = vectordb.as_retriever(
186
+ search_type="mmr",
187
+ search_kwargs={"k": 3, "lambda_mult": 0.5, "fetch_k": 15},
188
+ )
189
+
190
+ def format_docs(docs):
191
+ return "\n\n---\n\n".join(doc.page_content for doc in docs)
192
+
193
+ rag_core = (
194
+ RunnablePassthrough.assign(
195
+ context=lambda x: format_docs(retriever.invoke(x["question"]))
196
+ )
197
+ | prompt
198
+ | llm
199
+ | StrOutputParser()
200
+ )
201
+
202
+ chain_with_history = RunnableWithMessageHistory(
203
+ rag_core,
204
+ get_session_history,
205
+ input_messages_key="question",
206
+ history_messages_key="chat_history",
207
+ )
208
+
209
+ return chain_with_history, retriever
210
+
211
+
212
+ # ── Build once at startup (not per Gradio call) ───────────────────────────────
213
+ chain, retriever = build_chain(vectordb) # vectordb initialised elsewhere
214
+
215
+
216
+ # Query Re-write
217
+ def rewrite_query(question: str, llm) -> str:
218
+ """
219
+ Rewrites the user query to improve retrieval against POWERGRID
220
+ technical specification documents (IEC/IEEE standards, GIS/AIS
221
+ substation specs, protection & control documents).
222
+ """
223
+ rewrite_prompt = PromptTemplate.from_template("""
224
+ You are an expert query rewriter for a POWERGRID technical document retrieval system.
225
+ The document corpus contains:
226
+ - Model Technical Specifications for GIS/AIS substations (220kV / 400kV / 765kV)
227
+ - IEC and IEEE standards referenced in POWERGRID specs
228
+ - Equipment-specific specs: Circuit Breakers, Isolators, Surge Arresters, CTs, VTs,
229
+ Power Transformers, Reactors, Protection Relays, Control & Relay Panels
230
+ - Specific Requirements documents (which supersede other docs)
231
+
232
+ Your task:
233
+ 1. Expand abbreviations (e.g., CB → Circuit Breaker, SA → Surge Arrester, CT → Current Transformer)
234
+ 2. Add relevant technical keywords likely present in the documents
235
+ 3. Include clause/section indicators if the query implies a specific requirement
236
+ 4. If the query is vague, make it specific to power system substation context
237
+ 5. Preserve the original intent — do NOT change what is being asked
238
+ 6. Output ONLY the rewritten query, nothing else
239
+
240
+ Original Query: {question}
241
+
242
+ Rewritten Query:""")
243
+
244
+ chain = rewrite_prompt | llm | StrOutputParser()
245
+ rewritten = chain.invoke({"question": question})
246
+ return rewritten.strip()
247
+
248
+
249
+ ## RAG_PDF without re-writing query
250
+ def rag_pdf(question: str, chat_history: list, session_id: str = "default"):
251
+ response_text = chain.invoke(
252
+ {"question": question},
253
+ config={"configurable": {"session_id": session_id}},
254
+ )
255
+
256
+ source_docs = retriever.invoke(question)
257
+ source_docs = source_docs[:3]
258
+ references, unique_sources = get_file(source_docs)
259
+
260
+ if references:
261
+ response_text += "\n\n**References:**\n"
262
+ for i, ref in enumerate(references, 1): # numbered list, all 3
263
+ response_text += f"{i}. {ref}\n"
264
+
265
+ file_paths = [
266
+ os.path.realpath(os.path.join(folder_name, src))
267
+ for src in unique_sources
268
+ ]
269
+ return response_text, file_paths
270
+
271
+
272
+ # After query rewriter
273
+ def rag_pdf_query_rewrite(question: str, history: list):
274
+
275
+ # ── LLM (same instance used for rewriting + generation) ───────────────────
276
+ llm = ChatGroq(
277
+ model=GROQ_MODEL,
278
+ temperature=0.1,
279
+ max_tokens=2048,
280
+ api_key=GROQ_API_KEY,
281
+ )
282
+
283
+ # ── Step 1: Rewrite the query ──────────────────────────────────────────────
284
+ rewritten_question = rewrite_query(question, llm)
285
+ print(f"\n[Query Rewriter]\n Original : {question}\n Rewritten: {rewritten_question}\n")
286
+
287
+ # ── Step 2: Retrieve using rewritten query ─────────────────────────────────
288
+ source_docs = retriever.invoke(rewritten_question)
289
+ source_docs = source_docs[:3]
290
+
291
+ # ── Step 3: Build context from retrieved docs ──────────────────────────────
292
+ context = "\n\n---\n\n".join(doc.page_content for doc in source_docs)
293
+
294
+ # ── Step 4: Build prompt with original + rewritten query ───────────────────
295
+ system_instruction = (
296
+ "You are an expert **Electrical Engineer AI Assistant**, specialized in power systems "
297
+ "and substation design (AIS/GIS up to 765kV), providing insights strictly from the provided context.\n\n"
298
+ "**Formatting Guidelines:**\n"
299
+ "1. Organize using **bullet points or numbered lists** where appropriate.\n"
300
+ "2. **Bold** key technical terms, parameters, and essential facts.\n"
301
+ "3. Use technical language consistent with IEC/IEEE/POWERGRID standards.\n"
302
+ "4. For multi-step explanations use **sub-headings**.\n"
303
+ "5. **Always include clause references (e.g., Clause XX.XX) for every fact.**\n"
304
+ "6. **CRITICAL: Reproduce tables EXACTLY — preserve all rows, columns, headers. Never paraphrase table data.**\n\n"
305
+ "**Context Prioritization:**\n"
306
+ "1. Prioritize documents directly related to the queried equipment.\n"
307
+ "2. 'Specific Requirements' clauses supersede all other documents.\n"
308
+ "3. If context is insufficient: state 'The available documents do not contain information regarding [detail].'\n"
309
+ "4. Do not invent information outside the provided context."
310
+ )
311
+
312
+ prompt = ChatPromptTemplate.from_messages([
313
+ SystemMessagePromptTemplate.from_template(system_instruction),
314
+ MessagesPlaceholder(variable_name="chat_history"),
315
+ HumanMessagePromptTemplate.from_template(
316
+ "Context:\n{context}\n\n"
317
+ "Original Question: {original_question}\n"
318
+ "Rewritten Question: {rewritten_question}"
319
+ ),
320
+ ])
321
+
322
+ # ── Step 5: Convert history to LangChain messages ─────────────────────────
323
+ # Gradio 6 passes history as list of dicts: {"role": .., "content": ..}
324
+ from langchain_core.messages import HumanMessage, AIMessage
325
+ lc_history = []
326
+ for msg in history:
327
+ if msg["role"] == "user":
328
+ lc_history.append(HumanMessage(content=msg["content"]))
329
+ elif msg["role"] == "assistant":
330
+ lc_history.append(AIMessage(content=msg["content"]))
331
+
332
+ # ── Step 6: Generate response ──────────────────────────────────────────────
333
+ chain = prompt | llm | StrOutputParser()
334
+ response_text = chain.invoke({
335
+ "context": context,
336
+ "original_question": question,
337
+ "rewritten_question": rewritten_question,
338
+ "chat_history": lc_history,
339
+ })
340
+
341
+ # ── Step 7: Attach references ──────────────────────────────────────────────
342
+ references, unique_sources = get_file(source_docs)
343
+ if references:
344
+ response_text += "\n\n**References:**\n"
345
+ for i, ref in enumerate(references, 1):
346
+ response_text += f"{i}. {ref}\n"
347
+
348
+ file_paths = [
349
+ os.path.realpath(os.path.join(folder_name, src))
350
+ for src in unique_sources
351
+ ]
352
+
353
+ return response_text, file_paths
354
+
355
+
356
+ ## BACKEND Interface
357
+ # ── Pre-define components with render=False ────────────────────────────────────
358
+ file_output = gr.File(
359
+ render=False,
360
+ label="Reference Documents",
361
+ file_count="multiple",
362
+ interactive=False,
363
+ )
364
+
365
+ chatbot = gr.Chatbot(
366
+ render=False,
367
+ height=500,
368
+ show_label=False,
369
+ placeholder="Ask a question about POWERGRID Technical Specifications...",
370
+ layout="bubble",
371
+ )
372
+
373
+ # ── Wrapper function ───────────────────────────────────────────────────────────
374
+ def rag_pdf_ui(question: str, history: list) -> tuple:
375
+ response_text, file_paths = rag_pdf_query_rewrite(question, history)
376
+ return response_text, file_paths
377
+
378
+ # ── Text ───────────────────────────────────────────────────────────────────────
379
+ Title = "# Engineering SS — Model Technical Specifications"
380
+
381
+ Description = """
382
+ ## Welcome to the Search Engine for POWERGRID Model Technical Specifications! 👋
383
+
384
+ This tool leverages the power of AI to answer queries based on:
385
+
386
+ * **Model Technical Specifications** — POWERGRID Engineering Department 📑
387
+ * Includes latest Model TS as on date.
388
+
389
+ **Tips for Effective Use:**
390
+ * Use elaborate, keyword-rich questions for precise responses. 🎯
391
+ * Include equipment names (e.g., *GIS*, *Circuit Breaker*, *Surge Arrester*). 🔌
392
+ * Clear chat if responses seem off-context. 🔄
393
+ """
394
+
395
+ # ── Layout ─────────────────────────────────────────────────────────────────────
396
+ with gr.Blocks(
397
+ css="CSS/style.css",
398
+ fill_height=True,
399
+ theme=gr.themes.Base(),
400
+ ) as demo:
401
+
402
+ with gr.Column():
403
+
404
+ with gr.Row():
405
+ with gr.Column(scale=1):
406
+ gr.Image(
407
+ value="Images/PG Logo.png",
408
+ width=200,
409
+ show_label=False,
410
+ interactive=False,
411
+ elem_id="Logo",
412
+ buttons=[],
413
+ )
414
+ with gr.Column(scale=3, elem_classes=["center-title"]):
415
+ gr.Markdown(Title)
416
+
417
+ with gr.Row():
418
+ with gr.Column():
419
+ gr.Markdown(Description)
420
+
421
+ with gr.Row():
422
+ with gr.Column(elem_classes=["chat_container"]):
423
+
424
+ with gr.Tab("Model TS"):
425
+ gr.ChatInterface(
426
+ fn=rag_pdf_ui,
427
+ chatbot=chatbot,
428
+ title=None,
429
+ concurrency_limit=5,
430
+ fill_height=True,
431
+ delete_cache=(300, 360),
432
+ examples=[
433
+ "Type Tests for HV Switchgears.",
434
+ "What should be the height of GIB outside GIS hall for any type of crossings?",
435
+ "What is the resistivity of stone for ground spreading in switchyard?",
436
+ "Specify the details of Earthing System.",
437
+ "Specify details for Interpole cabling in CB.",
438
+ ],
439
+ additional_outputs=[file_output],
440
+ editable=True,
441
+ flagging_mode="never",
442
+ )
443
+ file_output.render()
444
+
445
+ # with gr.Tab("Pre-Bid Schemes"):
446
+ # gr.Markdown(
447
+ # "### Pre-Bid Scheme Query\n"
448
+ # "_Interface under development — coming soon._"
449
+ # )
450
+
451
+ demo.queue()