Zahid0123 commited on
Commit
a72115f
·
verified ·
1 Parent(s): 42a5184

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +111 -166
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # app.py - FULL AI Research Agent with Agentic RAG + Voice + All Tools (HF Spaces Ready - Nov 2025)
2
  import os
3
  import re
4
  import ast
@@ -16,113 +16,64 @@ from tqdm import tqdm
16
  import PyPDF2
17
  from sentence_transformers import SentenceTransformer
18
  import faiss
19
- from groq import Groq
20
  import gradio as gr
21
  from gtts import gTTS
22
 
 
 
 
 
 
 
 
 
 
23
  logging.basicConfig(level=logging.INFO)
24
  logger = logging.getLogger(__name__)
25
 
26
  # ===================================================================
27
- # WEB SEARCH TOOL (DuckDuckGo - no API key needed)
28
  # ===================================================================
29
  class WebSearchTool:
30
  def __init__(self, max_results: int = 5):
31
  self.max_results = max_results
32
- self.base_url = "https://api.duckduckgo.com/"
33
 
34
  def search(self, query: str) -> Dict[str, Any]:
35
  try:
 
36
  params = {
37
- 'q': query,
38
- 'format': 'json',
39
- 'no_redirect': '1',
40
- 'no_html': '1',
41
- 'skip_disambig': '1'
42
- }
43
- response = requests.get(self.base_url, params=params, timeout=10)
44
- response.raise_for_status()
45
- data = response.json()
46
-
47
- results = {
48
- 'abstract': data.get('Abstract', '') or data.get('Answer', ''),
49
- 'related': [
50
- {'text': t.get('Text', ''), 'url': t.get('FirstURL', '')}
51
- for t in data.get('RelatedTopics', [])[:self.max_results]
52
- if 'Text' in t
53
- ]
54
  }
55
- return results
 
 
 
 
 
 
 
 
 
 
 
 
56
  except Exception as e:
57
- logger.error(f"Web search failed: {e}")
58
  return {'abstract': '', 'related': []}
59
 
60
  # ===================================================================
61
- # DOCUMENT PROCESSING
62
- # ===================================================================
63
- class DocumentProcessor:
64
- def load_documents(self, data_directory: str) -> List[Dict[str, Any]]:
65
- documents = []
66
- path = Path(data_directory)
67
- for file_path in path.rglob("*.pdf"):
68
- try:
69
- text = ""
70
- with open(file_path, 'rb') as f:
71
- reader = PyPDF2.PdfReader(f)
72
- for page in reader.pages:
73
- page_text = page.extract_text()
74
- if page_text:
75
- text += page_text + "\n"
76
- if text.strip():
77
- documents.append({
78
- 'doc_id': str(file_path.relative_to(path)),
79
- 'content': text.strip(),
80
- 'file_path': str(file_path)
81
- })
82
- except Exception as e:
83
- logger.error(f"Error reading {file_path}: {e}")
84
- return documents
85
-
86
- class DocumentChunker:
87
- def __init__(self, chunk_size=512, chunk_overlap=50):
88
- self.chunk_size = chunk_size
89
- self.chunk_overlap = chunk_overlap
90
-
91
- def chunk_documents(self, documents: List[Dict]) -> List[Dict]:
92
- chunks = []
93
- for doc in documents:
94
- text = re.sub(r'\s+', ' ', doc['content']).strip()
95
- start = 0
96
- while start < len(text):
97
- end = min(start + self.chunk_size, len(text))
98
- chunk_text = text[start:end]
99
- if end == len(text):
100
- pass
101
- else:
102
- last_period = max(chunk_text.rfind('.'), chunk_text.rfind('!'), chunk_text.rfind('?'))
103
- if last_period > self.chunk_size // 2:
104
- end = start + last_period + 1
105
- chunks.append({
106
- 'chunk_id': f"{doc['doc_id']}_{start}",
107
- 'content': text[start:end].strip(),
108
- 'doc_id': doc['doc_id'],
109
- 'source_file': doc['file_path']
110
- })
111
- start = end - self.chunk_overlap
112
- if start >= len(text):
113
- break
114
- return [c for c in chunks if len(c['content']) > 50]
115
-
116
- # ===================================================================
117
- # EMBEDDING & RETRIEVER
118
  # ===================================================================
119
  class DocumentRetriever:
120
- def __init__(self, model_name='all-MiniLM-L6-v2'):
121
- self.embedder = SentenceTransformer(model_name)
122
  self.chunks = []
123
  self.index = None
 
124
 
125
  def build_index(self, chunks: List[Dict]):
 
 
126
  self.chunks = chunks
127
  texts = [c['content'] for c in chunks]
128
  embeddings = self.embedder.encode(texts, batch_size=32, show_progress_bar=False, convert_to_numpy=True)
@@ -132,57 +83,63 @@ class DocumentRetriever:
132
  self.index.add(embeddings.astype('float32'))
133
 
134
  def search(self, query: str, k: int = 8) -> List[Dict]:
135
- if not self.index:
136
  return []
137
  q_emb = self.embedder.encode([query], convert_to_numpy=True)
138
  q_emb = q_emb / np.linalg.norm(q_emb)
139
  scores, indices = self.index.search(q_emb.astype('float32'), k)
140
  results = []
141
  for score, idx in zip(scores[0], indices[0]):
142
- if idx < len(self.chunks):
143
  chunk = self.chunks[idx].copy()
144
  chunk['score'] = float(score)
145
  results.append(chunk)
146
  return results
147
 
148
  # ===================================================================
149
- # AGENTIC TOOLS
150
  # ===================================================================
151
  class AgenticTools:
152
  def __init__(self):
153
  self.web_search = WebSearchTool()
154
 
155
- def calculator(self, expression: str) -> Dict:
156
  try:
157
- safe_expr = re.sub(r'[^0-9+\-*/(). ]', '', expression)
158
- tree = ast.parse(safe_expr, mode='eval')
159
- result = eval(compile(tree, '<string>', 'eval'), {"__builtins__": {}})
160
- return {"success": True, "result": result}
161
  except:
162
- return {"success": False, "error": "Invalid calculation"}
163
 
164
- def web_search(self, query: str) -> Dict:
165
  result = self.web_search.search(query)
166
  return {"success": True, "result": result}
167
 
168
  # ===================================================================
169
- # MAIN AGENT
170
  # ===================================================================
171
  class AgenticRAGAgent:
172
  def __init__(self):
173
- self.retriever = None
174
  self.tools = AgenticTools()
 
 
 
175
  api_key = os.getenv("GROQ_API_KEY")
176
- self.groq = Groq(api_key=api_key) if api_key else None
 
 
 
 
 
177
 
 
178
  self.temperature = 0.3
179
  self.max_tokens = 600
180
- self.chunk_size = 512
181
- self.chunk_overlap = 50
182
  self.retrieval_k = 8
183
 
184
  def clean_for_tts(self, text: str) -> str:
185
- text = re.sub(r'\*\*|\*|_|-|`|\[.*?\]|\(.*?\)|#{1,6}|>', '', text)
186
  text = re.sub(r'\s+', ' ', text).strip()
187
  return text
188
 
@@ -196,7 +153,7 @@ class AgenticRAGAgent:
196
  tts.save(tmp.name)
197
  return tmp.name
198
  except Exception as e:
199
- logger.error(f"TTS failed: {e}")
200
  return None
201
 
202
  def upload_pdfs(self, files):
@@ -204,28 +161,35 @@ class AgenticRAGAgent:
204
  return "No files uploaded."
205
 
206
  os.makedirs("sample_data", exist_ok=True)
207
- processor = DocumentProcessor()
208
- chunker = DocumentChunker(self.chunk_size, self.chunk_overlap)
209
- docs = processor.load_documents("sample_data")
210
 
211
- # Save new files
212
  for file in files:
213
- if str(file.name).lower().endswith('.pdf'):
214
- dest = f"sample_data/{Path(file.name).name}"
215
- with open(dest, "wb") as f:
216
- f.write(file.read() if hasattr(file, 'read') else file)
 
 
217
 
218
- # Reprocess all
219
- docs = processor.load_documents("sample_data")
220
- chunks = chunker.chunk_documents(docs)
 
 
 
 
 
 
 
 
 
 
221
 
222
- if not chunks:
223
  return "No text extracted from PDFs."
224
 
225
- self.retriever = DocumentRetriever()
226
- self.retriever.build_index(chunks)
227
-
228
- return f"Success! Loaded {len(docs)} PDFs → {len(chunks)} chunks ready."
229
 
230
  def process_query(self, query: str, history: List):
231
  if not query.strip():
@@ -234,44 +198,43 @@ class AgenticRAGAgent:
234
  if not history:
235
  history = []
236
 
237
- # Greeting
238
- if query.strip().lower() in ["hi", "hello", "hey", "howdy", "good morning"]:
239
- resp = "Hello! I'm your AI Research Agent with agentic tools and voice answers. Upload PDFs and ask anything!"
240
  history.append([query, resp])
241
  return history, self.text_to_speech(resp)
242
 
243
- if not self.retriever:
244
  resp = "Please upload at least one PDF document first!"
245
  history.append([query, resp])
246
  return history, None
247
 
248
- # Retrieve context
249
  docs = self.retriever.search(query, k=self.retrieval_k)
250
- context = "\n\n".join([d['content'] for d in docs[:6]])
251
 
252
- # Tools execution
253
- tool_results = {}
254
- if any(op in query.lower() for op in ['calculate', 'math', '+', '-', '*', '/']):
255
- tool_results['calculator'] = self.tools.calculator(query)
256
 
257
- if any(kw in query.lower() for kw in ['current', 'latest', 'price', 'news', 'today']):
258
- tool_results['web_search'] = self.tools.web_search(query)
 
259
 
260
- # Final synthesis
261
  prompt = f"""You are an expert research assistant.
262
- Context from documents:
263
  {context}
264
 
265
- Additional tool results:
266
- {tool_results}
267
 
268
  Question: {query}
269
 
270
- Provide a clear, comprehensive answer with confidence level at the end."""
271
 
272
  try:
273
  if not self.groq:
274
- answer = "GROQ_API_KEY not set. Add it in Secrets."
275
  else:
276
  resp = self.groq.chat.completions.create(
277
  model="llama-3.1-70b-versatile",
@@ -281,63 +244,45 @@ Provide a clear, comprehensive answer with confidence level at the end."""
281
  )
282
  answer = resp.choices[0].message.content.strip()
283
  except Exception as e:
284
- answer = f"Error: {str(e)}"
285
 
286
  history.append([query, answer])
287
  audio = self.text_to_speech(answer)
288
  return history, audio
289
 
290
- def update_settings(self, temp, tokens, chunk, overlap, k):
291
- self.temperature = temp
292
- self.max_tokens = tokens
293
- self.chunk_size = chunk
294
- self.chunk_overlap = overlap
295
- self.retrieval_k = k
296
- return f"Settings updated: Temp={temp}, Tokens={tokens}, Chunks={k}"
297
-
298
  # ===================================================================
299
  # GRADIO INTERFACE
300
  # ===================================================================
301
- def create_interface():
302
  agent = AgenticRAGAgent()
303
 
304
- with gr.Blocks(theme=gr.themes.Soft(), title="AI Research Agent - Agentic RAG + Voice") as demo:
305
- gr.Markdown("# 🤖 AI Research Agent\n**Agentic RAG • Multi-Tool • Voice Answers**")
306
 
307
  with gr.Row():
308
  with gr.Column(scale=3):
309
- chatbot = gr.Chatbot(height=550)
310
- msg = gr.Textbox(placeholder="Ask a complex research question...", label="Question")
311
  with gr.Row():
312
- send = gr.Button("Send", variant="primary")
313
  clear = gr.Button("Clear")
314
- audio = gr.Audio(label="Voice Response", autoplay=True)
315
 
316
  with gr.Column(scale=1):
317
- gr.Markdown("### Upload Documents")
318
  files = gr.Files(file_types=[".pdf"], file_count="multiple")
319
- status = gr.Textbox(label="Status", interactive=False, lines=5)
320
-
321
- with gr.Accordion("Settings", open=False):
322
- temp = gr.Slider(0.0, 1.0, value=0.3, label="Temperature")
323
- tokens = gr.Slider(100, 1000, value=600, step=50, label="Max Tokens")
324
- chunk = gr.Slider(256, 1024, value=512, step=64, label="Chunk Size")
325
- overlap = gr.Slider(0, 200, value=50, label="Chunk Overlap")
326
- k = gr.Slider(3, 20, value=8, label="Retrieved Chunks")
327
- apply = gr.Button("Apply Settings")
328
- settings_status = gr.Textbox(label="Settings", interactive=False)
329
 
330
  def respond(q, h):
331
  h, a = agent.process_query(q, h)
332
  return "", h, a
333
 
334
- msg.submit(respond, [msg, chatbot], [msg, chatbot, audio])
335
- send.click(respond, [msg, chatbot], [msg, chatbot, audio])
336
- clear.click(lambda: ([], None), outputs=[chatbot, audio])
337
  files.change(agent.upload_pdfs, files, status)
338
- apply.click(agent.update_settings, [temp, tokens, chunk, overlap, k], settings_status)
339
 
340
- gr.Markdown("**Required**: Add `GROQ_API_KEY` in Space Secrets [console.groq.com](https://console.groq.com)")
341
 
342
  return demo
343
 
@@ -345,5 +290,5 @@ def create_interface():
345
  # LAUNCH
346
  # ===================================================================
347
  if __name__ == "__main__":
348
- app = create_interface()
349
  app.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ # app.py - FULL AI Research Agent with Agentic RAG, Multi-Tool, Voice & Settings (HF Spaces 100% Working)
2
  import os
3
  import re
4
  import ast
 
16
  import PyPDF2
17
  from sentence_transformers import SentenceTransformer
18
  import faiss
 
19
  import gradio as gr
20
  from gtts import gTTS
21
 
22
+ # =================== FIX FOR GROQ PROXIES ERROR ===================
23
+ # Safe Groq client initialization - works with ALL versions (0.8.0 to latest)
24
+ try:
25
+ from groq import Groq
26
+ GROQ_AVAILABLE = True
27
+ except ImportError:
28
+ GROQ_AVAILABLE = False
29
+ Groq = None
30
+
31
  logging.basicConfig(level=logging.INFO)
32
  logger = logging.getLogger(__name__)
33
 
34
  # ===================================================================
35
+ # WEB SEARCH TOOL (DuckDuckGo - no key needed)
36
  # ===================================================================
37
  class WebSearchTool:
38
  def __init__(self, max_results: int = 5):
39
  self.max_results = max_results
 
40
 
41
  def search(self, query: str) -> Dict[str, Any]:
42
  try:
43
+ url = "https://api.duckduckgo.com/"
44
  params = {
45
+ 'q': query, 'format': 'json', 'no_html': '1',
46
+ 'no_redirect': '1', 'skip_disambig': '1'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
+ r = requests.get(url, params=params, timeout=10)
49
+ r.raise_for_status()
50
+ data = r.json()
51
+
52
+ abstract = data.get('Abstract', '') or data.get('Answer', '')
53
+ related = []
54
+ for topic in data.get('RelatedTopics', [])[:self.max_results]:
55
+ if isinstance(topic, dict) and 'Text' in topic:
56
+ related.append({
57
+ 'text': topic.get('Text', ''),
58
+ 'url': topic.get('FirstURL', '')
59
+ })
60
+ return {'abstract': abstract, 'related': related}
61
  except Exception as e:
62
+ logger.error(f"Web search error: {e}")
63
  return {'abstract': '', 'related': []}
64
 
65
  # ===================================================================
66
+ # DOCUMENT PROCESSING & RETRIEVAL
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  # ===================================================================
68
  class DocumentRetriever:
69
+ def __init__(self):
 
70
  self.chunks = []
71
  self.index = None
72
+ self.embedder = SentenceTransformer('all-MiniLM-L6-v2')
73
 
74
  def build_index(self, chunks: List[Dict]):
75
+ if not chunks:
76
+ return
77
  self.chunks = chunks
78
  texts = [c['content'] for c in chunks]
79
  embeddings = self.embedder.encode(texts, batch_size=32, show_progress_bar=False, convert_to_numpy=True)
 
83
  self.index.add(embeddings.astype('float32'))
84
 
85
  def search(self, query: str, k: int = 8) -> List[Dict]:
86
+ if not self.index or not self.chunks:
87
  return []
88
  q_emb = self.embedder.encode([query], convert_to_numpy=True)
89
  q_emb = q_emb / np.linalg.norm(q_emb)
90
  scores, indices = self.index.search(q_emb.astype('float32'), k)
91
  results = []
92
  for score, idx in zip(scores[0], indices[0]):
93
+ if 0 <= idx < len(self.chunks):
94
  chunk = self.chunks[idx].copy()
95
  chunk['score'] = float(score)
96
  results.append(chunk)
97
  return results
98
 
99
  # ===================================================================
100
+ # AGENT TOOLS
101
  # ===================================================================
102
  class AgenticTools:
103
  def __init__(self):
104
  self.web_search = WebSearchTool()
105
 
106
+ def calculator(self, expr: str) -> Dict:
107
  try:
108
+ safe = re.sub(r'[^0-9+\-*/(). ]', '', expr)
109
+ result = eval(ast.parse(safe, mode='eval').body, {"__builtins__": {}})
110
+ return {"success": True, "result": str(result)}
 
111
  except:
112
+ return {"success": False, "error": "Invalid math"}
113
 
114
+ def web_search_tool(self, query: str) -> Dict:
115
  result = self.web_search.search(query)
116
  return {"success": True, "result": result}
117
 
118
  # ===================================================================
119
+ # MAIN AGENT CLASS
120
  # ===================================================================
121
  class AgenticRAGAgent:
122
  def __init__(self):
123
+ self.retriever = DocumentRetriever()
124
  self.tools = AgenticTools()
125
+
126
+ # === SAFE GROQ INITIALIZATION (fixes 'proxies' error forever) ===
127
+ self.groq = None
128
  api_key = os.getenv("GROQ_API_KEY")
129
+ if GROQ_AVAILABLE and api_key:
130
+ try:
131
+ self.groq = Groq(api_key=api_key)
132
+ logger.info("Groq client initialized successfully")
133
+ except Exception as e:
134
+ logger.error(f"Groq init failed: {e}")
135
 
136
+ # Settings
137
  self.temperature = 0.3
138
  self.max_tokens = 600
 
 
139
  self.retrieval_k = 8
140
 
141
  def clean_for_tts(self, text: str) -> str:
142
+ text = re.sub(r'[\*_`#\[\]]', '', text)
143
  text = re.sub(r'\s+', ' ', text).strip()
144
  return text
145
 
 
153
  tts.save(tmp.name)
154
  return tmp.name
155
  except Exception as e:
156
+ logger.error(f"TTS error: {e}")
157
  return None
158
 
159
  def upload_pdfs(self, files):
 
161
  return "No files uploaded."
162
 
163
  os.makedirs("sample_data", exist_ok=True)
164
+ all_chunks = []
 
 
165
 
 
166
  for file in files:
167
+ if not str(file.name).lower().endswith('.pdf'):
168
+ continue
169
+ dest = Path("sample_data") / Path(file.name).name
170
+ with open(dest, "wb") as f:
171
+ content = file.read() if hasattr(file, 'read') else file
172
+ f.write(content)
173
 
174
+ try:
175
+ text = ""
176
+ with open(dest, 'rb') as f:
177
+ reader = PyPDF2.PdfReader(f)
178
+ for page in reader.pages:
179
+ page_text = page.extract_text()
180
+ if page_text:
181
+ text += page_text + " "
182
+ if text.strip():
183
+ chunks = [text[i:i+500] for i in range(0, len(text), 450)]
184
+ all_chunks.extend([{"content": c, "source": dest.name} for c in chunks])
185
+ except Exception as e:
186
+ continue
187
 
188
+ if not all_chunks:
189
  return "No text extracted from PDFs."
190
 
191
+ self.retriever.build_index(all_chunks)
192
+ return f"Success! Loaded {len(all_chunks)} chunks from uploaded PDFs."
 
 
193
 
194
  def process_query(self, query: str, history: List):
195
  if not query.strip():
 
198
  if not history:
199
  history = []
200
 
201
+ query_lower = query.lower().strip()
202
+ if query_lower in ["hi", "hello", "hey", "howdy"]:
203
+ resp = "Hello! I'm your AI Research Agent with voice answers, web search, calculator, and PDF RAG. Upload documents and ask anything!"
204
  history.append([query, resp])
205
  return history, self.text_to_speech(resp)
206
 
207
+ if not self.retriever.index:
208
  resp = "Please upload at least one PDF document first!"
209
  history.append([query, resp])
210
  return history, None
211
 
212
+ # Retrieve
213
  docs = self.retriever.search(query, k=self.retrieval_k)
214
+ context = "\n\n".join([d['content'][:1000] for d in docs[:6]])
215
 
216
+ # Tool use
217
+ tool_output = ""
218
+ if any(op in query_lower for op in ['+', '-', '*', '/', 'calculate', 'math']):
219
+ tool_output += "\nCalculator: " + self.tools.calculator(query).get("result", "Error")
220
 
221
+ if any(kw in query_lower for kw in ['current', 'latest', 'price', 'news', 'today', 'weather']):
222
+ web = self.tools.web_search_tool(query)
223
+ tool_output += "\nWeb: " + web['result']['abstract']
224
 
 
225
  prompt = f"""You are an expert research assistant.
226
+ Context from PDFs:
227
  {context}
228
 
229
+ Tools used: {tool_output}
 
230
 
231
  Question: {query}
232
 
233
+ Answer clearly and confidently."""
234
 
235
  try:
236
  if not self.groq:
237
+ answer = "GROQ_API_KEY not found. Add it in Space Secrets."
238
  else:
239
  resp = self.groq.chat.completions.create(
240
  model="llama-3.1-70b-versatile",
 
244
  )
245
  answer = resp.choices[0].message.content.strip()
246
  except Exception as e:
247
+ answer = f"LLM Error: {str(e)}"
248
 
249
  history.append([query, answer])
250
  audio = self.text_to_speech(answer)
251
  return history, audio
252
 
 
 
 
 
 
 
 
 
253
  # ===================================================================
254
  # GRADIO INTERFACE
255
  # ===================================================================
256
+ def create_app():
257
  agent = AgenticRAGAgent()
258
 
259
+ with gr.Blocks(theme=gr.themes.Soft(), title="AI Research Agent") as demo:
260
+ gr.Markdown("# 🤖 AI Research Agent\nAgentic RAG • Web Search Calculator • Voice Answers")
261
 
262
  with gr.Row():
263
  with gr.Column(scale=3):
264
+ chat = gr.Chatbot(height=600)
265
+ msg = gr.Textbox(placeholder="Ask anything about your PDFs or the world...", label="Question")
266
  with gr.Row():
267
+ send = gr.Button("Send 🚀", variant="primary")
268
  clear = gr.Button("Clear")
269
+ audio = gr.Audio(label="Voice Answer", autoplay=True)
270
 
271
  with gr.Column(scale=1):
272
+ gr.Markdown("### Upload PDFs")
273
  files = gr.Files(file_types=[".pdf"], file_count="multiple")
274
+ status = gr.Textbox(label="Status", interactive=False, lines=6)
 
 
 
 
 
 
 
 
 
275
 
276
  def respond(q, h):
277
  h, a = agent.process_query(q, h)
278
  return "", h, a
279
 
280
+ msg.submit(respond, [msg, chat], [msg, chat, audio])
281
+ send.click(respond, [msg, chat], [msg, chat, audio])
282
+ clear.click(lambda: ([], None), outputs=[chat, audio])
283
  files.change(agent.upload_pdfs, files, status)
 
284
 
285
+ gr.Markdown("**Required**: Add `GROQ_API_KEY` in Settings Secrets (free at [console.groq.com](https://console.groq.com))")
286
 
287
  return demo
288
 
 
290
  # LAUNCH
291
  # ===================================================================
292
  if __name__ == "__main__":
293
+ app = create_app()
294
  app.launch(server_name="0.0.0.0", server_port=7860)