Ina-Shapiro commited on
Commit
028ef27
Β·
1 Parent(s): a213258

Refactor app.py to enhance paper fetching functionality and improve error handling. Update README.md to reflect new features and usage instructions. Remove dotenv dependency from requirements.txt.

Browse files
Files changed (3) hide show
  1. README.md +23 -13
  2. app.py +205 -256
  3. requirements.txt +0 -1
README.md CHANGED
@@ -16,14 +16,13 @@ A modern conversational AI chatbot designed specifically for exploring and analy
16
 
17
  ## ✨ Latest Features
18
 
19
- - πŸ“– **Full Paper Access**: Access complete paper texts from the Papers directory
20
- - πŸ” **Intelligent Paper Search**: Automatically finds relevant papers based on user queries
21
- - 🧠 **Smart Conversation Memory**: Maintains chat history with intelligent truncation
22
  - πŸš€ **Real-time Streaming**: Instant response streaming for better UX
23
  - πŸŽ›οΈ **Multiple Model Selection**: Choose between GPT-4o, GPT-4o-mini, and GPT-3.5 Turbo
24
  - βš™οΈ **Advanced Parameters**: Fine-tune temperature, max tokens, and top-p
25
  - 🎨 **Modern UI**: Responsive design with intuitive controls
26
- - πŸ”§ **Customizable System Messages**: Define AI personality and behavior
27
  - πŸ›‘οΈ **Robust Error Handling**: Clear error messages for common issues
28
  - πŸ“± **Mobile Responsive**: Works great on all devices
29
 
@@ -45,11 +44,21 @@ pip install -r requirements.txt
45
  ### 3. Configure Environment
46
 
47
  #### For Local Development
48
- Create a `.env` file in the project root:
49
 
 
 
 
 
 
 
 
 
 
 
 
50
  ```bash
51
- # .env
52
- OPENAI_API_KEY=your_openai_api_key_here
53
  ```
54
 
55
  #### For Hugging Face Spaces Deployment
@@ -78,10 +87,11 @@ The chatbot will be available at `http://localhost:7860`
78
  ## 🎯 Usage Guide
79
 
80
  ### Basic Paper Exploration
81
- 1. **Ask about specific topics**: "Find papers about transformer architecture"
82
- 2. **Request full papers**: "Show me the full paper about pigs"
83
- 3. **Get detailed information**: "What's the conclusion of the pig disease paper?"
84
- 4. **Ask for quotes**: "Quote the methodology section from the pig paper"
 
85
 
86
  ### Advanced Controls
87
 
@@ -172,7 +182,7 @@ Papers/
172
  ### Common Issues
173
 
174
  **API Key Errors**
175
- - Ensure your `.env` file contains a valid OpenAI API key
176
  - Check that the API key has sufficient credits
177
  - For Hugging Face Spaces: Verify the secret is named `OPENAI_API_KEY`
178
 
@@ -194,7 +204,7 @@ Papers/
194
  - Long conversations are automatically truncated
195
 
196
  ### Error Messages
197
- - **"Invalid API key"**: Check your `.env` file or Hugging Face Spaces secrets
198
  - **"Quota exceeded"**: Add credits to your OpenAI account
199
  - **"Rate limit"**: Wait and retry
200
  - **"Paper not found"**: Check that the paper file exists in the Papers directory
 
16
 
17
  ## ✨ Latest Features
18
 
19
+ - πŸ“– **Smart Function Calling**: Intelligent paper retrieval using OpenAI's function calling API
20
+ - πŸ” **Dynamic Paper Fetching**: Automatically fetches full paper texts when needed
21
+ - 🧠 **Contextual Conversation Memory**: Maintains chat history with intelligent truncation
22
  - πŸš€ **Real-time Streaming**: Instant response streaming for better UX
23
  - πŸŽ›οΈ **Multiple Model Selection**: Choose between GPT-4o, GPT-4o-mini, and GPT-3.5 Turbo
24
  - βš™οΈ **Advanced Parameters**: Fine-tune temperature, max tokens, and top-p
25
  - 🎨 **Modern UI**: Responsive design with intuitive controls
 
26
  - πŸ›‘οΈ **Robust Error Handling**: Clear error messages for common issues
27
  - πŸ“± **Mobile Responsive**: Works great on all devices
28
 
 
44
  ### 3. Configure Environment
45
 
46
  #### For Local Development
47
+ Set your OpenAI API key as an environment variable:
48
 
49
+ **Windows (PowerShell):**
50
+ ```powershell
51
+ $env:OPENAI_API_KEY="your_openai_api_key_here"
52
+ ```
53
+
54
+ **Windows (Command Prompt):**
55
+ ```cmd
56
+ set OPENAI_API_KEY=your_openai_api_key_here
57
+ ```
58
+
59
+ **Linux/macOS:**
60
  ```bash
61
+ export OPENAI_API_KEY="your_openai_api_key_here"
 
62
  ```
63
 
64
  #### For Hugging Face Spaces Deployment
 
87
  ## 🎯 Usage Guide
88
 
89
  ### Basic Paper Exploration
90
+ 1. **Ask about specific topics**: "What papers discuss AI's impact on employment?"
91
+ 2. **Request full papers**: "Show me the full paper about AI companions"
92
+ 3. **Get detailed information**: "What's the conclusion of the pig disease detection paper?"
93
+ 4. **Compare findings**: "Compare findings on AI in education"
94
+ 5. **Ask for specific details**: "What methodology did they use in the pig disease paper?"
95
 
96
  ### Advanced Controls
97
 
 
182
  ### Common Issues
183
 
184
  **API Key Errors**
185
+ - Ensure your `OPENAI_API_KEY` environment variable is set correctly
186
  - Check that the API key has sufficient credits
187
  - For Hugging Face Spaces: Verify the secret is named `OPENAI_API_KEY`
188
 
 
204
  - Long conversations are automatically truncated
205
 
206
  ### Error Messages
207
+ - **"Invalid API key"**: Check your environment variable or Hugging Face Spaces secrets
208
  - **"Quota exceeded"**: Add credits to your OpenAI account
209
  - **"Rate limit"**: Wait and retry
210
  - **"Paper not found"**: Check that the paper file exists in the Papers directory
app.py CHANGED
@@ -2,13 +2,9 @@ import gradio as gr
2
  import os
3
  import json
4
  import re
5
- from typing import Iterator, Dict, Any, List
6
  from openai import OpenAI
7
  from openai.types.chat import ChatCompletionChunk
8
- from dotenv import load_dotenv
9
-
10
- # Load environment variables
11
- load_dotenv()
12
 
13
  # Load abstracts content once at startup
14
  def load_abstracts_content():
@@ -19,108 +15,81 @@ def load_abstracts_content():
19
  except FileNotFoundError:
20
  return "Abstracts database not found."
21
 
22
- # Load full paper texts
23
- def load_paper_texts():
24
- """Load all paper texts from the Papers directory."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  papers = {}
26
  papers_dir = "Papers"
27
 
28
  if not os.path.exists(papers_dir):
29
- return papers
30
 
31
- for filename in os.listdir(papers_dir):
32
- if filename.endswith('.txt'):
33
- filepath = os.path.join(papers_dir, filename)
 
 
 
 
 
34
  try:
35
  with open(filepath, "r", encoding="utf-8") as f:
36
- content = f.read()
37
- # Extract title from filename (remove .txt extension)
38
- title = filename[:-4]
39
- papers[title] = content
40
  except Exception as e:
41
- print(f"Error loading {filename}: {e}")
 
 
42
 
43
  return papers
44
 
45
- # Load abstracts content globally
46
- ABSTRACTS_CONTENT = load_abstracts_content()
47
- PAPER_TEXTS = load_paper_texts()
48
-
49
- def search_papers(query: str, papers: Dict[str, str]) -> List[tuple[str, str, str]]:
50
- """
51
- Search through paper texts for relevant content.
52
- Returns list of (title, content, relevance_score) tuples.
53
- """
54
- results = []
55
- query_lower = query.lower()
56
-
57
- for title, content in papers.items():
58
- # Simple keyword matching - can be enhanced with more sophisticated search
59
- relevance_score = 0
60
-
61
- # Check if query terms appear in title
62
- if any(term in title.lower() for term in query_lower.split()):
63
- relevance_score += 10
64
-
65
- # Check if query terms appear in content
66
- content_lower = content.lower()
67
- for term in query_lower.split():
68
- if term in content_lower:
69
- relevance_score += content_lower.count(term)
70
-
71
- if relevance_score > 0:
72
- # For full paper requests, include more content
73
- if any(keyword in query.lower() for keyword in ["full paper", "complete paper", "entire paper", "show me the paper", "read the paper"]):
74
- # Include more content for full paper requests
75
- truncated_content = content[:8000] + "..." if len(content) > 8000 else content
76
- else:
77
- # Truncate content to first 2000 characters for context
78
- truncated_content = content[:2000] + "..." if len(content) > 2000 else content
79
- results.append((title, truncated_content, relevance_score))
80
-
81
- # Sort by relevance score
82
- results.sort(key=lambda x: x[2], reverse=True)
83
- return results[:3] # Return top 3 most relevant papers
84
-
85
- def get_relevant_papers_content(user_query: str) -> str:
86
- """
87
- Get relevant paper content based on user query.
88
- """
89
- if not PAPER_TEXTS:
90
- return ""
91
-
92
- relevant_papers = search_papers(user_query, PAPER_TEXTS)
93
-
94
- if not relevant_papers:
95
- return ""
96
-
97
- content = "\n\n=== FULL PAPER CONTENT ===\n"
98
- for title, paper_content, score in relevant_papers:
99
- content += f"\n--- {title} ---\n"
100
- content += paper_content
101
- content += "\n" + "="*50 + "\n"
102
-
103
- return content
104
-
105
- def get_full_paper_content(paper_title: str) -> str:
106
- """
107
- Get the full content of a specific paper by title.
108
- """
109
- if not PAPER_TEXTS:
110
- return ""
111
-
112
- # Try to find the paper by title (case-insensitive)
113
- for title, content in PAPER_TEXTS.items():
114
- if paper_title.lower() in title.lower() or title.lower() in paper_title.lower():
115
- return f"\n\n=== FULL PAPER: {title} ===\n\n{content}"
116
-
117
- return ""
118
-
119
  def extract_conclusion_from_paper(content: str) -> str:
120
- """
121
- Extract the conclusion section from a paper's content.
122
- """
123
- # Look for conclusion sections with more specific patterns
124
  conclusion_patterns = [
125
  "conclusion and future works",
126
  "conclusion and future work",
@@ -133,11 +102,9 @@ def extract_conclusion_from_paper(content: str) -> str:
133
  lines = content.split('\n')
134
  conclusion_start = -1
135
 
136
- # First, try to find a proper conclusion section
137
  for i, line in enumerate(lines):
138
  line_lower = line.lower().strip()
139
  if any(pattern in line_lower for pattern in conclusion_patterns):
140
- # Check if it's a section header
141
  if (line.isupper() or
142
  line.strip().endswith(':') or
143
  len(line.strip()) < 100 or
@@ -146,83 +113,29 @@ def extract_conclusion_from_paper(content: str) -> str:
146
  break
147
 
148
  if conclusion_start != -1:
149
- # Extract from conclusion start to acknowledgments or references
150
  conclusion_lines = []
151
  for line in lines[conclusion_start:]:
152
  line_stripped = line.strip()
153
- # Stop at acknowledgments or references
154
  if (line_stripped.lower().startswith('acknowledgments') or
155
  line_stripped.lower().startswith('references') or
156
  line_stripped.startswith('--- Page')):
157
  break
158
  conclusion_lines.append(line)
159
 
160
- conclusion_text = '\n'.join(conclusion_lines)
161
- return conclusion_text
162
-
163
- # If no conclusion section found, look for the final paragraphs
164
- # Find the last substantial paragraph (usually before references or acknowledgments)
165
- lines_reversed = list(reversed(lines))
166
- final_content_start = -1
167
-
168
- for i, line in enumerate(lines_reversed):
169
- line_stripped = line.strip()
170
- # Skip empty lines and page markers
171
- if (line_stripped and
172
- not line_stripped.startswith('--- Page') and
173
- not line_stripped.startswith('References') and
174
- not line_stripped.lower().startswith('acknowledgments')):
175
- # Look for the last substantial paragraph
176
- if len(line_stripped) > 50: # Substantial line
177
- final_content_start = len(lines) - i
178
- break
179
-
180
- if final_content_start != -1:
181
- # Get the last 1500 characters from the final content
182
- final_content = '\n'.join(lines[final_content_start:])
183
- return final_content[-1500:] if len(final_content) > 1500 else final_content
184
 
185
  # Fallback: return the last 1000 characters
186
  return content[-1000:] if len(content) > 1000 else content
187
 
188
- # Get API key with better error handling
189
- api_key = os.getenv("OPENAI_API_KEY")
190
- if not api_key:
191
- print("⚠️ Warning: OPENAI_API_KEY environment variable not set!")
192
- print("Please set your OpenAI API key as an environment variable.")
193
- print("For Hugging Face Spaces: Add OPENAI_API_KEY as a repository secret")
194
- print("For local development: Create a .env file with OPENAI_API_KEY=your_key")
195
- # Create a dummy client for UI to load (will show error when used)
196
- client = None
197
- else:
198
- # Initialize OpenAI client with latest configuration
199
- client = OpenAI(
200
- api_key=api_key,
201
- timeout=60.0, # 60 second timeout
202
- max_retries=3 # Retry failed requests up to 3 times
203
- )
204
-
205
- # Available models
206
- AVAILABLE_MODELS = {
207
- "GPT-4o-mini": "gpt-4o-mini",
208
- "GPT-4o": "gpt-4o",
209
- "GPT-3.5 Turbo": "gpt-3.5-turbo"
210
- }
211
-
212
  def truncate_conversation_history(messages: list, max_tokens: int = 8000) -> list:
213
- """
214
- Truncate conversation history to stay within token limits.
215
- Keeps the most recent messages and system message.
216
- """
217
- if len(messages) <= 3: # System + 1 user + 1 assistant
218
  return messages
219
 
220
- # Always keep system message
221
  system_message = messages[0]
222
  conversation_messages = messages[1:]
223
 
224
- # Keep only the most recent messages
225
- while len(conversation_messages) > 6: # Keep last 3 exchanges
226
  conversation_messages = conversation_messages[2:]
227
 
228
  return [system_message] + conversation_messages
@@ -236,65 +149,39 @@ def respond(
236
  top_p: float,
237
  ) -> Iterator[str]:
238
  """
239
- Generate a response using OpenAI's latest models.
240
- Maintains conversation history in the messages array with proper truncation.
241
  """
242
  if not client:
243
- yield "❌ Error: OpenAI API key not configured. Please set the OPENAI_API_KEY environment variable or add it as a repository secret in Hugging Face Spaces."
244
  return
245
 
246
  if not message.strip():
247
  yield "Please enter a message to start the conversation."
248
  return
249
 
250
- # Get relevant full paper content based on user query
251
- relevant_papers_content = get_relevant_papers_content(message)
252
-
253
- # Check if user is asking for a specific paper (e.g., "show me the full paper about pigs")
254
- specific_paper_content = ""
255
- conclusion_content = ""
256
-
257
- if any(keyword in message.lower() for keyword in ["full paper", "complete paper", "entire paper", "show me the paper", "read the paper"]):
258
- # Try to find specific paper content
259
- for title in PAPER_TEXTS.keys():
260
- if any(term in title.lower() for term in message.lower().split()):
261
- specific_paper_content = get_full_paper_content(title)
262
- break
263
-
264
- # Check if user is asking for conclusions specifically
265
- if any(keyword in message.lower() for keyword in ["conclusion", "conclusions", "what's the conclusion", "what is the conclusion"]):
266
- for title, content in PAPER_TEXTS.items():
267
- if any(term in title.lower() for term in message.lower().split()):
268
- conclusion_text = extract_conclusion_from_paper(content)
269
- conclusion_content = f"\n\n=== CONCLUSION FROM {title} ===\n\n{conclusion_text}"
270
- break
271
-
272
- # Initialize messages array with system message
273
- # Use the pre-loaded abstracts content and relevant full papers
274
- system_prompt = f"""You are an AI chatbot designed to help users explore and analyze AI research papers. Your primary function is to retrieve relevant papers and answer questions about them based solely on the provided paper database.
275
 
276
- Here is the current research paper database:
 
 
277
 
 
278
  {ABSTRACTS_CONTENT}
279
 
280
- {relevant_papers_content}
281
-
282
- {specific_paper_content}
283
-
284
- {conclusion_content}
285
-
286
- IMPORTANT INSTRUCTIONS:
287
- 1. When users ask for specific details, conclusions, or quotes from papers, use the full paper content provided above to give accurate, detailed responses.
288
- 2. If the full paper content is available, you can quote directly from it and provide comprehensive answers including conclusions, methodology, and specific findings.
289
- 3. When asked for conclusions, look for sections titled "Conclusion", "Conclusions", or the final paragraphs of the paper.
290
- 4. When asked for quotes, provide the exact text from the paper content provided.
291
- 5. You can now access the complete text of papers and provide detailed information including conclusions, methodology, and specific quotes.
292
- 6. If a user asks for the "full paper" or "complete paper", provide a comprehensive summary including all major sections (abstract, introduction, methodology, results, conclusions).
293
- 7. When conclusion content is specifically provided, use that content to answer conclusion-related questions."""
294
 
295
  messages = [{"role": "system", "content": system_prompt}]
296
 
297
- # Add conversation history to messages array
298
  for user_msg, assistant_msg in history:
299
  if user_msg and user_msg.strip():
300
  messages.append({"role": "user", "content": user_msg.strip()})
@@ -304,55 +191,134 @@ IMPORTANT INSTRUCTIONS:
304
  # Add current user message
305
  messages.append({"role": "user", "content": message.strip()})
306
 
307
- # Truncate conversation if it gets too long
308
  messages = truncate_conversation_history(messages)
309
 
310
  try:
311
- # Get the actual model identifier
312
  model = AVAILABLE_MODELS.get(model_name, "gpt-4o-mini")
313
 
314
- # Generate response using the selected model
315
  response = client.chat.completions.create(
316
  model=model,
317
  messages=messages,
318
  max_tokens=max_tokens,
319
  temperature=temperature,
320
  top_p=top_p,
321
- stream=True,
322
- # Additional parameters for better control
323
- presence_penalty=0.0,
324
- frequency_penalty=0.0
325
  )
326
 
327
- # Stream the response with proper error handling
328
- response_text = ""
 
 
 
329
  for chunk in response:
330
- if hasattr(chunk.choices[0], 'delta') and chunk.choices[0].delta.content is not None:
331
- response_text += chunk.choices[0].delta.content
332
- yield response_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
 
334
  except Exception as e:
335
  error_message = f"Error: {str(e)}"
336
  if "api_key" in str(e).lower():
337
- error_message = "Error: Invalid or missing OpenAI API key. Please check your configuration."
338
  elif "quota" in str(e).lower():
339
- error_message = "Error: API quota exceeded. Please check your OpenAI account."
340
  elif "rate" in str(e).lower():
341
- error_message = "Error: Rate limit exceeded. Please wait a moment and try again."
342
  yield error_message
343
 
344
  def chat_fn(message, history, model_name, max_tokens, temperature, top_p):
345
- """
346
- Single function that handles the entire chat interaction.
347
- This is the proper way to handle chatbot interfaces in Gradio.
348
- """
349
  if not message.strip():
350
  return history
351
 
352
- # Add user message to history
353
  history.append([message, ""])
354
 
355
- # Generate response
356
  for response in respond(message, history[:-1], model_name, max_tokens, temperature, top_p):
357
  history[-1][1] = response
358
  yield history
@@ -361,7 +327,7 @@ def clear_history() -> tuple:
361
  """Clear the conversation history."""
362
  return [], ""
363
 
364
- # Create the Gradio interface with latest features
365
  with gr.Blocks(
366
  title="πŸ“š AI Research Paper Chatbot",
367
  theme=gr.themes.Soft(),
@@ -376,21 +342,18 @@ with gr.Blocks(
376
  """
377
  # πŸ“š AI Research Paper Chatbot
378
 
379
- Chat with an AI assistant designed to help you explore and analyze AI research papers. The chatbot maintains conversation history to provide contextual responses.
380
 
381
  **Features:**
382
- - πŸ“– Research paper analysis and retrieval
383
- - πŸ’¬ Conversation memory with smart truncation
384
- - πŸš€ Real-time streaming responses
385
  - πŸŽ›οΈ Multiple model selection
386
- - βš™οΈ Customizable parameters
387
- - 🎨 Modern, responsive UI
388
  """
389
  )
390
 
391
  with gr.Row():
392
  with gr.Column(scale=3):
393
- # Chat interface
394
  chatbot = gr.Chatbot(
395
  height=500,
396
  show_label=False,
@@ -409,7 +372,6 @@ with gr.Blocks(
409
  clear_btn = gr.Button("Clear", variant="secondary", scale=1)
410
 
411
  with gr.Column(scale=1):
412
- # Control panel
413
  gr.Markdown("### βš™οΈ Settings")
414
 
415
  model_dropdown = gr.Dropdown(
@@ -434,7 +396,7 @@ with gr.Blocks(
434
  value=0.7,
435
  step=0.1,
436
  label="Temperature",
437
- info="Creativity level (0.0 = focused, 2.0 = creative)"
438
  )
439
 
440
  top_p_slider = gr.Slider(
@@ -446,22 +408,20 @@ with gr.Blocks(
446
  info="Response diversity"
447
  )
448
 
449
- # Example messages
450
  gr.Markdown("### πŸ’‘ Examples")
451
- example_btn1 = gr.Button("Find papers about transformer architecture", size="sm")
452
- example_btn2 = gr.Button("What are the latest developments in reinforcement learning?", size="sm")
453
- example_btn3 = gr.Button("Summarize research on large language models", size="sm")
454
- example_btn4 = gr.Button("Show me the full paper about pigs", size="sm")
455
- example_btn5 = gr.Button("What's the conclusion of the pig disease paper?", size="sm")
456
 
457
- # Simple event handling with proper chat function
458
  msg.submit(
459
  chat_fn,
460
  [msg, chatbot, model_dropdown, max_tokens_slider, temperature_slider, top_p_slider],
461
  [chatbot],
462
  show_progress=True
463
  ).then(
464
- lambda: "", # Clear input
465
  outputs=[msg]
466
  )
467
 
@@ -471,37 +431,26 @@ with gr.Blocks(
471
  [chatbot],
472
  show_progress=True
473
  ).then(
474
- lambda: "", # Clear input
475
  outputs=[msg]
476
  )
477
 
478
  clear_btn.click(clear_history, outputs=[chatbot, msg])
479
 
480
- # Example button handlers
481
- example_btn1.click(lambda: "Find papers about transformer architecture", outputs=msg)
482
- example_btn2.click(lambda: "What are the latest developments in reinforcement learning?", outputs=msg)
483
- example_btn3.click(lambda: "Summarize research on large language models", outputs=msg)
484
- example_btn4.click(lambda: "Show me the full paper about pigs", outputs=msg)
485
- example_btn5.click(lambda: "What's the conclusion of the pig disease paper?", outputs=msg)
486
 
487
  if __name__ == "__main__":
488
- # Check if API key is set
489
  if not os.getenv("OPENAI_API_KEY"):
490
  print("⚠️ Warning: OPENAI_API_KEY environment variable not set!")
491
- print("Please set your OpenAI API key as an environment variable.")
492
- print("For Hugging Face Spaces: Add OPENAI_API_KEY as a repository secret")
493
- print("For local development: Create a .env file with OPENAI_API_KEY=your_key")
494
- print("\nTo get an API key:")
495
- print("1. Visit https://platform.openai.com/api-keys")
496
- print("2. Sign in or create an account")
497
- print("3. Generate a new API key")
498
- print("4. Add it to your .env file or Hugging Face Spaces secrets")
499
 
500
- # Launch with proper configuration for Hugging Face Spaces
501
  demo.launch(
502
  server_name="0.0.0.0",
503
  server_port=7860,
504
- share=False, # Disable sharing to avoid issues
505
  show_error=True,
506
  quiet=False
507
  )
 
2
  import os
3
  import json
4
  import re
5
+ from typing import Iterator, Dict, Any, List, Optional
6
  from openai import OpenAI
7
  from openai.types.chat import ChatCompletionChunk
 
 
 
 
8
 
9
  # Load abstracts content once at startup
10
  def load_abstracts_content():
 
15
  except FileNotFoundError:
16
  return "Abstracts database not found."
17
 
18
+ # Load abstracts content globally
19
+ ABSTRACTS_CONTENT = load_abstracts_content()
20
+
21
+ # Get API key with better error handling
22
+ api_key = os.getenv("OPENAI_API_KEY")
23
+ if not api_key:
24
+ print("⚠️ Warning: OPENAI_API_KEY environment variable not set!")
25
+ client = None
26
+ else:
27
+ client = OpenAI(
28
+ api_key=api_key,
29
+ timeout=60.0,
30
+ max_retries=3
31
+ )
32
+
33
+ # Available models
34
+ AVAILABLE_MODELS = {
35
+ "GPT-4o-mini": "gpt-4o-mini",
36
+ "GPT-4o": "gpt-4o",
37
+ "GPT-3.5 Turbo": "gpt-3.5-turbo"
38
+ }
39
+
40
+ # Define the tool for fetching papers
41
+ FETCH_PAPERS_TOOL = {
42
+ "type": "function",
43
+ "function": {
44
+ "name": "fetch_papers",
45
+ "description": "Fetch full text content of research papers by their filenames. Use this when you need detailed information, full text, conclusions, methodology, or specific quotes from papers.",
46
+ "parameters": {
47
+ "type": "object",
48
+ "properties": {
49
+ "filenames": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "string"
53
+ },
54
+ "description": "List of paper filenames to fetch (e.g., ['The Labor Market Effects of Generativ.txt', 'AI Companions Reduce Loneliness.txt'])"
55
+ }
56
+ },
57
+ "required": ["filenames"]
58
+ }
59
+ }
60
+ }
61
+
62
+ def fetch_papers(filenames: List[str]) -> Dict[str, str]:
63
+ """
64
+ Fetch full paper texts by filenames.
65
+ Returns a dictionary mapping filename to content.
66
+ """
67
  papers = {}
68
  papers_dir = "Papers"
69
 
70
  if not os.path.exists(papers_dir):
71
+ return {"error": "Papers directory not found"}
72
 
73
+ for filename in filenames:
74
+ # Ensure .txt extension
75
+ if not filename.endswith('.txt'):
76
+ filename += '.txt'
77
+
78
+ filepath = os.path.join(papers_dir, filename)
79
+
80
+ if os.path.exists(filepath):
81
  try:
82
  with open(filepath, "r", encoding="utf-8") as f:
83
+ papers[filename] = f.read()
 
 
 
84
  except Exception as e:
85
+ papers[filename] = f"Error loading paper: {str(e)}"
86
+ else:
87
+ papers[filename] = f"Paper not found: {filename}"
88
 
89
  return papers
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  def extract_conclusion_from_paper(content: str) -> str:
92
+ """Extract the conclusion section from a paper's content."""
 
 
 
93
  conclusion_patterns = [
94
  "conclusion and future works",
95
  "conclusion and future work",
 
102
  lines = content.split('\n')
103
  conclusion_start = -1
104
 
 
105
  for i, line in enumerate(lines):
106
  line_lower = line.lower().strip()
107
  if any(pattern in line_lower for pattern in conclusion_patterns):
 
108
  if (line.isupper() or
109
  line.strip().endswith(':') or
110
  len(line.strip()) < 100 or
 
113
  break
114
 
115
  if conclusion_start != -1:
 
116
  conclusion_lines = []
117
  for line in lines[conclusion_start:]:
118
  line_stripped = line.strip()
 
119
  if (line_stripped.lower().startswith('acknowledgments') or
120
  line_stripped.lower().startswith('references') or
121
  line_stripped.startswith('--- Page')):
122
  break
123
  conclusion_lines.append(line)
124
 
125
+ return '\n'.join(conclusion_lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
 
127
  # Fallback: return the last 1000 characters
128
  return content[-1000:] if len(content) > 1000 else content
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  def truncate_conversation_history(messages: list, max_tokens: int = 8000) -> list:
131
+ """Truncate conversation history to stay within token limits."""
132
+ if len(messages) <= 3:
 
 
 
133
  return messages
134
 
 
135
  system_message = messages[0]
136
  conversation_messages = messages[1:]
137
 
138
+ while len(conversation_messages) > 6:
 
139
  conversation_messages = conversation_messages[2:]
140
 
141
  return [system_message] + conversation_messages
 
149
  top_p: float,
150
  ) -> Iterator[str]:
151
  """
152
+ Generate a response using OpenAI's models with function calling.
 
153
  """
154
  if not client:
155
+ yield "❌ Error: OpenAI API key not configured."
156
  return
157
 
158
  if not message.strip():
159
  yield "Please enter a message to start the conversation."
160
  return
161
 
162
+ # Initialize messages with a concise system prompt
163
+ system_prompt = f"""You are an AI chatbot designed to help users explore and analyze AI research papers.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ You have access to:
166
+ 1. An abstracts database with summaries of research papers
167
+ 2. A tool to fetch full paper texts when needed
168
 
169
+ ABSTRACTS DATABASE:
170
  {ABSTRACTS_CONTENT}
171
 
172
+ INSTRUCTIONS:
173
+ - Answer questions using the abstracts when possible
174
+ - Use the fetch_papers tool when users ask for:
175
+ - Full papers or complete papers
176
+ - Specific details not in abstracts
177
+ - Conclusions, methodology, or quotes
178
+ - Any information requiring the full text
179
+ - When fetching papers, use the exact filename from the abstracts table
180
+ - Provide accurate, detailed responses based on the actual paper content"""
 
 
 
 
 
181
 
182
  messages = [{"role": "system", "content": system_prompt}]
183
 
184
+ # Add conversation history
185
  for user_msg, assistant_msg in history:
186
  if user_msg and user_msg.strip():
187
  messages.append({"role": "user", "content": user_msg.strip()})
 
191
  # Add current user message
192
  messages.append({"role": "user", "content": message.strip()})
193
 
194
+ # Truncate if needed
195
  messages = truncate_conversation_history(messages)
196
 
197
  try:
 
198
  model = AVAILABLE_MODELS.get(model_name, "gpt-4o-mini")
199
 
200
+ # Initial response with tool support
201
  response = client.chat.completions.create(
202
  model=model,
203
  messages=messages,
204
  max_tokens=max_tokens,
205
  temperature=temperature,
206
  top_p=top_p,
207
+ tools=[FETCH_PAPERS_TOOL],
208
+ tool_choice="auto",
209
+ stream=True
 
210
  )
211
 
212
+ # Collect the response and handle tool calls
213
+ full_response = ""
214
+ tool_calls = []
215
+ current_tool_call = None
216
+
217
  for chunk in response:
218
+ if hasattr(chunk.choices[0], 'delta'):
219
+ delta = chunk.choices[0].delta
220
+
221
+ # Handle regular content
222
+ if delta.content is not None:
223
+ full_response += delta.content
224
+ yield full_response
225
+
226
+ # Handle tool calls
227
+ if delta.tool_calls:
228
+ for tool_call_chunk in delta.tool_calls:
229
+ if tool_call_chunk.id:
230
+ # New tool call
231
+ if current_tool_call:
232
+ tool_calls.append(current_tool_call)
233
+ current_tool_call = {
234
+ "id": tool_call_chunk.id,
235
+ "type": "function",
236
+ "function": {
237
+ "name": tool_call_chunk.function.name if tool_call_chunk.function else "",
238
+ "arguments": ""
239
+ }
240
+ }
241
+
242
+ if current_tool_call and tool_call_chunk.function:
243
+ if tool_call_chunk.function.arguments:
244
+ current_tool_call["function"]["arguments"] += tool_call_chunk.function.arguments
245
+
246
+ # Add final tool call if exists
247
+ if current_tool_call:
248
+ tool_calls.append(current_tool_call)
249
+
250
+ # Process tool calls if any
251
+ if tool_calls:
252
+ # Add the assistant's message with tool calls
253
+ messages.append({
254
+ "role": "assistant",
255
+ "content": full_response if full_response else None,
256
+ "tool_calls": tool_calls
257
+ })
258
+
259
+ # Execute tool calls
260
+ for tool_call in tool_calls:
261
+ function_name = tool_call["function"]["name"]
262
+
263
+ if function_name == "fetch_papers":
264
+ try:
265
+ # Parse arguments
266
+ arguments = json.loads(tool_call["function"]["arguments"])
267
+ filenames = arguments.get("filenames", [])
268
+
269
+ # Fetch papers
270
+ papers_content = fetch_papers(filenames)
271
+
272
+ # Add tool response to messages
273
+ tool_response = {
274
+ "role": "tool",
275
+ "tool_call_id": tool_call["id"],
276
+ "content": json.dumps(papers_content)
277
+ }
278
+ messages.append(tool_response)
279
+
280
+ except Exception as e:
281
+ tool_response = {
282
+ "role": "tool",
283
+ "tool_call_id": tool_call["id"],
284
+ "content": f"Error: {str(e)}"
285
+ }
286
+ messages.append(tool_response)
287
+
288
+ # Get final response with tool results
289
+ final_response = client.chat.completions.create(
290
+ model=model,
291
+ messages=messages,
292
+ max_tokens=max_tokens,
293
+ temperature=temperature,
294
+ top_p=top_p,
295
+ stream=True
296
+ )
297
+
298
+ # Stream the final response
299
+ final_text = ""
300
+ for chunk in final_response:
301
+ if hasattr(chunk.choices[0], 'delta') and chunk.choices[0].delta.content is not None:
302
+ final_text += chunk.choices[0].delta.content
303
+ yield full_response + "\n\n" + final_text if full_response else final_text
304
 
305
  except Exception as e:
306
  error_message = f"Error: {str(e)}"
307
  if "api_key" in str(e).lower():
308
+ error_message = "Error: Invalid or missing OpenAI API key."
309
  elif "quota" in str(e).lower():
310
+ error_message = "Error: API quota exceeded."
311
  elif "rate" in str(e).lower():
312
+ error_message = "Error: Rate limit exceeded."
313
  yield error_message
314
 
315
  def chat_fn(message, history, model_name, max_tokens, temperature, top_p):
316
+ """Handle the entire chat interaction."""
 
 
 
317
  if not message.strip():
318
  return history
319
 
 
320
  history.append([message, ""])
321
 
 
322
  for response in respond(message, history[:-1], model_name, max_tokens, temperature, top_p):
323
  history[-1][1] = response
324
  yield history
 
327
  """Clear the conversation history."""
328
  return [], ""
329
 
330
+ # Create the Gradio interface
331
  with gr.Blocks(
332
  title="πŸ“š AI Research Paper Chatbot",
333
  theme=gr.themes.Soft(),
 
342
  """
343
  # πŸ“š AI Research Paper Chatbot
344
 
345
+ Chat with an AI assistant that can intelligently retrieve and analyze research papers.
346
 
347
  **Features:**
348
+ - πŸ“– Smart paper retrieval using function calling
349
+ - πŸ’¬ Contextual conversation memory
350
+ - πŸš€ Efficient token usage
351
  - πŸŽ›οΈ Multiple model selection
 
 
352
  """
353
  )
354
 
355
  with gr.Row():
356
  with gr.Column(scale=3):
 
357
  chatbot = gr.Chatbot(
358
  height=500,
359
  show_label=False,
 
372
  clear_btn = gr.Button("Clear", variant="secondary", scale=1)
373
 
374
  with gr.Column(scale=1):
 
375
  gr.Markdown("### βš™οΈ Settings")
376
 
377
  model_dropdown = gr.Dropdown(
 
396
  value=0.7,
397
  step=0.1,
398
  label="Temperature",
399
+ info="Creativity level"
400
  )
401
 
402
  top_p_slider = gr.Slider(
 
408
  info="Response diversity"
409
  )
410
 
 
411
  gr.Markdown("### πŸ’‘ Examples")
412
+ example_btn1 = gr.Button("What papers discuss AI's impact on employment?", size="sm")
413
+ example_btn2 = gr.Button("Show me the full paper about AI companions", size="sm")
414
+ example_btn3 = gr.Button("What's the conclusion of the pig disease detection paper?", size="sm")
415
+ example_btn4 = gr.Button("Compare findings on AI in education", size="sm")
 
416
 
417
+ # Event handlers
418
  msg.submit(
419
  chat_fn,
420
  [msg, chatbot, model_dropdown, max_tokens_slider, temperature_slider, top_p_slider],
421
  [chatbot],
422
  show_progress=True
423
  ).then(
424
+ lambda: "",
425
  outputs=[msg]
426
  )
427
 
 
431
  [chatbot],
432
  show_progress=True
433
  ).then(
434
+ lambda: "",
435
  outputs=[msg]
436
  )
437
 
438
  clear_btn.click(clear_history, outputs=[chatbot, msg])
439
 
440
+ # Example handlers
441
+ example_btn1.click(lambda: "What papers discuss AI's impact on employment?", outputs=msg)
442
+ example_btn2.click(lambda: "Show me the full paper about AI companions", outputs=msg)
443
+ example_btn3.click(lambda: "What's the conclusion of the pig disease detection paper?", outputs=msg)
444
+ example_btn4.click(lambda: "Compare findings on AI in education", outputs=msg)
 
445
 
446
  if __name__ == "__main__":
 
447
  if not os.getenv("OPENAI_API_KEY"):
448
  print("⚠️ Warning: OPENAI_API_KEY environment variable not set!")
 
 
 
 
 
 
 
 
449
 
 
450
  demo.launch(
451
  server_name="0.0.0.0",
452
  server_port=7860,
453
+ share=False,
454
  show_error=True,
455
  quiet=False
456
  )
requirements.txt CHANGED
@@ -1,4 +1,3 @@
1
  openai>=1.98.0
2
  gradio==4.44.0
3
- python-dotenv>=1.0.0
4
  pydantic==2.10.6
 
1
  openai>=1.98.0
2
  gradio==4.44.0
 
3
  pydantic==2.10.6