milwright commited on
Commit
6bfe64d
Β·
verified Β·
1 Parent(s): 847bfa6

Upload 4 files

Browse files
Files changed (3) hide show
  1. README.md +3 -4
  2. app.py +214 -105
  3. config.json +6 -6
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Britannica Wiki Search
3
  emoji: πŸ€–
4
  colorFrom: blue
5
  colorTo: red
@@ -7,10 +7,9 @@ sdk: gradio
7
  sdk_version: 5.35.0
8
  app_file: app.py
9
  pinned: false
10
- short_description: Mini LLMs w/ URL grounding in Wikipedia + Britannica
11
  ---
12
 
13
- # Britannica Wiki Search
14
 
15
 
16
 
@@ -54,7 +53,7 @@ short_description: Mini LLMs w/ URL grounding in Wikipedia + Britannica
54
 
55
  ## Configuration
56
 
57
- - **Model**: openai/gpt-4o-mini-search-preview
58
  - **Temperature**: 0.7
59
  - **Max Tokens**: 1500
60
  - **API Key Variable**: OPENROUTER_API_KEY
 
1
  ---
2
+ title: Socratic Aid
3
  emoji: πŸ€–
4
  colorFrom: blue
5
  colorTo: red
 
7
  sdk_version: 5.35.0
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
+ # Socratic Aid
13
 
14
 
15
 
 
53
 
54
  ## Configuration
55
 
56
+ - **Model**: google/gemini-2.0-flash-001
57
  - **Temperature**: 0.7
58
  - **Max Tokens**: 1500
59
  - **API Key Variable**: OPENROUTER_API_KEY
app.py CHANGED
@@ -10,14 +10,14 @@ import urllib.parse
10
 
11
 
12
  # Configuration
13
- SPACE_NAME = "Britannica Wiki Search"
14
  SPACE_DESCRIPTION = ""
15
- SYSTEM_PROMPT = """You are a research aid specializing in academic literature search and analysis. Your expertise spans discovering peer-reviewed sources, assessing research methodologies, synthesizing findings across studies, and delivering properly formatted citations. When responding, anchor claims in specific sources from provided URL contexts, differentiate between direct evidence and interpretive analysis, and note any limitations or contradictory results. Employ clear, accessible language that demystifies complex research, and propose connected research directions when appropriate. Your purpose is to serve as an informed research tool supporting users through initial concept development, exploratory investigation, information collection, and source compilation."""
16
- MODEL = "openai/gpt-4o-mini-search-preview"
17
- GROUNDING_URLS = ["https://www.wikipedia.org/", "https://www.britannica.com/"]
18
  # Get access code from environment variable for security
19
  ACCESS_CODE = os.environ.get("SPACE_ACCESS_CODE", "")
20
- ENABLE_DYNAMIC_URLS = True
21
  ENABLE_VECTOR_RAG = False
22
  RAG_DATA = None
23
 
@@ -63,63 +63,79 @@ def validate_url_domain(url):
63
  try:
64
  from urllib.parse import urlparse
65
  parsed = urlparse(url)
66
- # Allow common domains and exclude potentially dangerous ones
67
- allowed_patterns = [
68
- 'wikipedia.org', 'britannica.com', 'edu', 'gov', 'arxiv.org',
69
- 'pubmed.ncbi.nlm.nih.gov', 'scholar.google.com', 'researchgate.net'
70
- ]
71
- return any(pattern in parsed.netloc.lower() for pattern in allowed_patterns)
72
  except:
73
- return False
 
74
 
75
  def fetch_url_content(url):
76
- """Fetch content from URL with enhanced error handling"""
77
  if not validate_url_domain(url):
78
- return f"URL domain not in allowed list for security: {url}"
79
 
80
  try:
 
81
  headers = {
82
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
 
 
 
 
83
  }
84
- response = requests.get(url, headers=headers, timeout=10)
 
85
  response.raise_for_status()
 
86
 
87
- # Parse HTML and extract text
88
- soup = BeautifulSoup(response.text, 'html.parser')
 
89
 
90
- # Remove script and style elements
91
- for script in soup(["script", "style"]):
92
- script.decompose()
93
 
94
- # Get text and clean it up
95
- text = soup.get_text()
96
  lines = (line.strip() for line in text.splitlines())
97
  chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
98
- text = ' '.join(chunk for chunk in chunks if chunk)
99
 
100
- # Limit content length
101
  if len(text) > 4000:
102
- text = text[:4000] + "..."
 
 
 
 
 
 
 
103
 
104
- return text
 
105
  except requests.exceptions.RequestException as e:
106
  return f"Error fetching {url}: {str(e)}"
107
  except Exception as e:
108
- return f"Error processing {url}: {str(e)}"
109
 
110
  def extract_urls_from_text(text):
111
- """Extract URLs from text using regex"""
112
- url_pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
 
113
  urls = re.findall(url_pattern, text)
114
 
115
- # Filter out URLs that are clearly not useful
116
- filtered_urls = []
117
  for url in urls:
118
- # Basic filtering - must contain a dot and be reasonably long
 
 
119
  if '.' in url and len(url) > 10:
120
- filtered_urls.append(url)
121
 
122
- return filtered_urls
123
 
124
  # Global cache for URL content to avoid re-crawling in generated spaces
125
  _url_content_cache = {}
@@ -158,41 +174,62 @@ def export_conversation_to_markdown(conversation_history):
158
 
159
  markdown_content = f"""# Conversation Export
160
  Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
161
- Space: {SPACE_NAME}
162
 
163
  ---
164
 
165
  """
166
 
 
167
  for i, message in enumerate(conversation_history):
168
  if isinstance(message, dict):
169
- # New format: {"role": "user", "content": "..."}
170
- role = message.get("role", "unknown")
171
- content = message.get("content", "")
172
 
173
- if role == "user":
174
- markdown_content += f"## πŸ‘€ User\n\n{content}\n\n"
175
- elif role == "assistant":
176
- markdown_content += f"## πŸ€– Assistant\n\n{content}\n\n"
 
177
  elif isinstance(message, (list, tuple)) and len(message) >= 2:
178
- # Legacy format: [user_msg, assistant_msg]
 
179
  user_msg, assistant_msg = message[0], message[1]
180
  if user_msg:
181
- markdown_content += f"## πŸ‘€ User\n\n{user_msg}\n\n"
182
  if assistant_msg:
183
- markdown_content += f"## πŸ€– Assistant\n\n{assistant_msg}\n\n"
184
-
185
- markdown_content += f"""
186
-
187
- ---
188
-
189
- *Exported from {SPACE_NAME} on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
190
- """
191
 
192
  return markdown_content
193
 
194
- # Global variable to store chat history for export
195
- chat_history_store = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
 
197
  def generate_response(message, history):
198
  """Generate response using OpenRouter API"""
@@ -222,8 +259,9 @@ def generate_response(message, history):
222
  if ENABLE_DYNAMIC_URLS:
223
  urls_in_message = extract_urls_from_text(message)
224
  if urls_in_message:
 
225
  dynamic_context_parts = []
226
- for url in urls_in_message[:2]: # Limit to 2 URLs to avoid overwhelming context
227
  content = fetch_url_content(url)
228
  dynamic_context_parts.append(f"\n\nDynamic context from {url}:\n{content}")
229
  if dynamic_context_parts:
@@ -232,19 +270,16 @@ def generate_response(message, history):
232
  # Build enhanced system prompt with grounding context
233
  enhanced_system_prompt = SYSTEM_PROMPT + grounding_context
234
 
235
- # Build conversation history for API
236
- messages = [{
237
- "role": "system",
238
- "content": enhanced_system_prompt
239
- }]
240
 
241
- # Add conversation history - Support both new messages format and legacy tuple format
242
  for chat in history:
243
  if isinstance(chat, dict):
244
- # New format: {"role": "user", "content": "..."}
245
  messages.append(chat)
246
  elif isinstance(chat, (list, tuple)) and len(chat) >= 2:
247
- # Legacy format: ("user msg", "bot msg")
248
  user_msg, assistant_msg = chat[0], chat[1]
249
  if user_msg:
250
  messages.append({"role": "user", "content": user_msg})
@@ -254,8 +289,12 @@ def generate_response(message, history):
254
  # Add current message
255
  messages.append({"role": "user", "content": message})
256
 
 
257
  try:
258
- # Make API request to OpenRouter
 
 
 
259
  response = requests.post(
260
  url="https://openrouter.ai/api/v1/chat/completions",
261
  headers={
@@ -268,18 +307,38 @@ def generate_response(message, history):
268
  "model": MODEL,
269
  "messages": messages,
270
  "temperature": 0.7,
271
- "max_tokens": 1500,
272
- "stream": False
273
  },
274
  timeout=30
275
  )
276
 
 
 
277
  if response.status_code == 200:
278
  try:
279
- response_data = response.json()
280
- assistant_response = response_data['choices'][0]['message']['content']
281
- print(f"βœ… API request successful")
282
- return assistant_response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  except (KeyError, IndexError, json.JSONDecodeError) as e:
284
  print(f"❌ Failed to parse API response: {str(e)}")
285
  return f"API Error: Failed to parse response - {str(e)}"
@@ -296,64 +355,110 @@ def generate_response(message, history):
296
  elif response.status_code == 429:
297
  error_msg = f"⏱️ **Rate Limit Exceeded**\n\n"
298
  error_msg += f"Too many requests. Please wait a moment and try again.\n\n"
299
- error_msg += f"If this persists, check your OpenRouter plan limits."
 
 
 
300
  print(f"❌ Rate limit exceeded: {response.status_code}")
301
  return error_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  else:
303
  error_msg = f"🚫 **API Error {response.status_code}**\n\n"
304
  error_msg += f"An unexpected error occurred. Please try again.\n\n"
305
- error_msg += f"If this persists, the issue may be with the OpenRouter service."
 
 
 
306
  print(f"❌ API error: {response.status_code} - {response.text[:200]}")
307
  return error_msg
308
 
309
  except requests.exceptions.Timeout:
310
  error_msg = f"⏰ **Request Timeout**\n\n"
311
- error_msg += f"The request took too long to complete. Please try again with a shorter message."
312
- print(f"❌ Request timeout")
 
 
 
 
313
  return error_msg
314
  except requests.exceptions.ConnectionError:
315
  error_msg = f"🌐 **Connection Error**\n\n"
316
- error_msg += f"Unable to connect to the AI service. Please check your internet connection and try again."
317
- print(f"❌ Connection error")
 
 
 
 
318
  return error_msg
319
  except Exception as e:
320
- error_msg = f"πŸ’₯ **Unexpected Error**\n\n"
321
- error_msg += f"An unexpected error occurred: {str(e)}\n\n"
 
322
  error_msg += f"Please try again or contact support if this persists."
323
  print(f"❌ Unexpected error: {str(e)}")
324
  return error_msg
325
 
326
- # Global state for access control
327
  access_granted = gr.State(False)
 
328
 
329
  def verify_access_code(code):
330
  """Verify the access code"""
331
  global _access_granted_global
332
  if not ACCESS_CODE:
333
- return gr.update(visible=False), gr.update(visible=True), True
 
334
 
335
- if code.strip() == ACCESS_CODE.strip():
336
  _access_granted_global = True
337
- return gr.update(visible=False), gr.update(visible=True), True
338
  else:
339
- return gr.update(value="❌ Incorrect access code. Please try again.", visible=True), gr.update(visible=False), False
 
340
 
341
  def protected_generate_response(message, history):
342
- """Wrapper that checks access before generating response"""
343
- global _access_granted_global
344
  if ACCESS_CODE and not _access_granted_global:
345
- return "Access denied. Please enter the correct access code."
346
  return generate_response(message, history)
347
 
 
 
 
348
  def store_and_generate_response(message, history):
349
- """Wrapper that stores conversation and generates response"""
350
  global chat_history_store
351
 
352
- # Generate response
353
  response = protected_generate_response(message, history)
354
 
355
- # Store in global history for export (using new format)
356
- chat_history_store = history + [{"role": "user", "content": message}, {"role": "assistant", "content": response}]
 
 
 
 
 
 
 
 
 
 
357
 
358
  return response
359
 
@@ -365,21 +470,29 @@ def export_current_conversation():
365
  markdown_content = export_conversation_to_markdown(chat_history_store)
366
 
367
  # Save to temporary file
368
- with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
369
  f.write(markdown_content)
370
  temp_file = f.name
371
 
372
  return gr.update(value=temp_file, visible=True)
373
 
374
  def export_conversation(history):
375
- """Export conversation from gradio history"""
376
  if not history:
377
- return "No conversation to export."
 
 
378
 
379
- return export_conversation_to_markdown(history)
 
 
 
 
 
380
 
 
381
  def get_configuration_status():
382
- """Get current configuration status for display"""
383
  status_parts = []
384
 
385
  if API_KEY_VALID:
@@ -407,9 +520,6 @@ def get_configuration_status():
407
 
408
  return "\n".join(status_parts)
409
 
410
- # Global access tracking
411
- _access_granted_global = not bool(ACCESS_CODE) # If no access code, grant access
412
-
413
  # Create interface with access code protection
414
  with gr.Blocks(title=SPACE_NAME) as demo:
415
  gr.Markdown(f"# {SPACE_NAME}")
@@ -419,11 +529,10 @@ with gr.Blocks(title=SPACE_NAME) as demo:
419
  with gr.Accordion("πŸ“Š Configuration Status", open=not API_KEY_VALID):
420
  gr.Markdown(get_configuration_status())
421
 
422
- # Access code section (only visible if ACCESS_CODE is set)
423
- if ACCESS_CODE:
424
- with gr.Column(visible=True) as access_section:
425
- gr.Markdown("πŸ” **Access Required**")
426
- gr.Markdown("Please enter the access code to use this space.")
427
 
428
  access_input = gr.Textbox(
429
  label="Access Code",
@@ -468,4 +577,4 @@ with gr.Blocks(title=SPACE_NAME) as demo:
468
  )
469
 
470
  if __name__ == "__main__":
471
- demo.launch()
 
10
 
11
 
12
  # Configuration
13
+ SPACE_NAME = "Socratic Aid"
14
  SPACE_DESCRIPTION = ""
15
+ SYSTEM_PROMPT = """You are a pedagogically-minded academic assistant designed for introductory courses. Your approach follows constructivist learning principles: build on students' prior knowledge, scaffold complex concepts through graduated questioning, and use Socratic dialogue to guide discovery. Provide concise, evidence-based explanations that connect theory to lived experiences. Each response should model critical thinking by acknowledging multiple perspectives, identifying assumptions, and revealing conceptual relationships. Conclude with open-ended questions that promote higher-order thinkingβ€”analysis, synthesis, or evaluationβ€”rather than recall."""
16
+ MODEL = "google/gemini-2.0-flash-001"
17
+ GROUNDING_URLS = []
18
  # Get access code from environment variable for security
19
  ACCESS_CODE = os.environ.get("SPACE_ACCESS_CODE", "")
20
+ ENABLE_DYNAMIC_URLS = False
21
  ENABLE_VECTOR_RAG = False
22
  RAG_DATA = None
23
 
 
63
  try:
64
  from urllib.parse import urlparse
65
  parsed = urlparse(url)
66
+ # Check for valid domain structure
67
+ if parsed.netloc and '.' in parsed.netloc:
68
+ return True
 
 
 
69
  except:
70
+ pass
71
+ return False
72
 
73
  def fetch_url_content(url):
74
+ """Enhanced URL content fetching with improved compatibility and error handling"""
75
  if not validate_url_domain(url):
76
+ return f"Invalid URL format: {url}"
77
 
78
  try:
79
+ # Enhanced headers for better compatibility
80
  headers = {
81
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
82
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
83
+ 'Accept-Language': 'en-US,en;q=0.5',
84
+ 'Accept-Encoding': 'gzip, deflate',
85
+ 'Connection': 'keep-alive'
86
  }
87
+
88
+ response = requests.get(url, timeout=15, headers=headers)
89
  response.raise_for_status()
90
+ soup = BeautifulSoup(response.content, 'html.parser')
91
 
92
+ # Enhanced content cleaning
93
+ for element in soup(["script", "style", "nav", "header", "footer", "aside", "form", "button"]):
94
+ element.decompose()
95
 
96
+ # Extract main content preferentially
97
+ main_content = soup.find('main') or soup.find('article') or soup.find('div', class_=lambda x: bool(x and 'content' in x.lower())) or soup
98
+ text = main_content.get_text()
99
 
100
+ # Enhanced text cleaning
 
101
  lines = (line.strip() for line in text.splitlines())
102
  chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
103
+ text = ' '.join(chunk for chunk in chunks if chunk and len(chunk) > 2)
104
 
105
+ # Smart truncation - try to end at sentence boundaries
106
  if len(text) > 4000:
107
+ truncated = text[:4000]
108
+ last_period = truncated.rfind('.')
109
+ if last_period > 3000: # If we can find a reasonable sentence break
110
+ text = truncated[:last_period + 1]
111
+ else:
112
+ text = truncated + "..."
113
+
114
+ return text if text.strip() else "No readable content found at this URL"
115
 
116
+ except requests.exceptions.Timeout:
117
+ return f"Timeout error fetching {url} (15s limit exceeded)"
118
  except requests.exceptions.RequestException as e:
119
  return f"Error fetching {url}: {str(e)}"
120
  except Exception as e:
121
+ return f"Error processing content from {url}: {str(e)}"
122
 
123
  def extract_urls_from_text(text):
124
+ """Extract URLs from text using regex with enhanced validation"""
125
+ import re
126
+ url_pattern = r'https?://[^\s<>"{}|\\^`\[\]"]+'
127
  urls = re.findall(url_pattern, text)
128
 
129
+ # Basic URL validation and cleanup
130
+ validated_urls = []
131
  for url in urls:
132
+ # Remove trailing punctuation that might be captured
133
+ url = url.rstrip('.,!?;:')
134
+ # Basic domain validation
135
  if '.' in url and len(url) > 10:
136
+ validated_urls.append(url)
137
 
138
+ return validated_urls
139
 
140
  # Global cache for URL content to avoid re-crawling in generated spaces
141
  _url_content_cache = {}
 
174
 
175
  markdown_content = f"""# Conversation Export
176
  Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
 
177
 
178
  ---
179
 
180
  """
181
 
182
+ message_pair_count = 0
183
  for i, message in enumerate(conversation_history):
184
  if isinstance(message, dict):
185
+ role = message.get('role', 'unknown')
186
+ content = message.get('content', '')
 
187
 
188
+ if role == 'user':
189
+ message_pair_count += 1
190
+ markdown_content += f"## User Message {message_pair_count}\n\n{content}\n\n"
191
+ elif role == 'assistant':
192
+ markdown_content += f"## Assistant Response {message_pair_count}\n\n{content}\n\n---\n\n"
193
  elif isinstance(message, (list, tuple)) and len(message) >= 2:
194
+ # Handle legacy tuple format: ["user msg", "assistant msg"]
195
+ message_pair_count += 1
196
  user_msg, assistant_msg = message[0], message[1]
197
  if user_msg:
198
+ markdown_content += f"## User Message {message_pair_count}\n\n{user_msg}\n\n"
199
  if assistant_msg:
200
+ markdown_content += f"## Assistant Response {message_pair_count}\n\n{assistant_msg}\n\n---\n\n"
 
 
 
 
 
 
 
201
 
202
  return markdown_content
203
 
204
+ # Initialize RAG context if enabled
205
+ if ENABLE_VECTOR_RAG and RAG_DATA:
206
+ try:
207
+ import faiss
208
+ import numpy as np
209
+ import base64
210
+
211
+ class SimpleRAGContext:
212
+ def __init__(self, rag_data):
213
+ # Deserialize FAISS index
214
+ index_bytes = base64.b64decode(rag_data['index_base64'])
215
+ self.index = faiss.deserialize_index(index_bytes)
216
+
217
+ # Restore chunks and mappings
218
+ self.chunks = rag_data['chunks']
219
+ self.chunk_ids = rag_data['chunk_ids']
220
+
221
+ def get_context(self, query, max_chunks=3):
222
+ """Get relevant context - simplified version"""
223
+ # In production, you'd compute query embedding here
224
+ # For now, return a simple message
225
+ return "\n\n[RAG context would be retrieved here based on similarity search]\n\n"
226
+
227
+ rag_context_provider = SimpleRAGContext(RAG_DATA)
228
+ except Exception as e:
229
+ print(f"Failed to initialize RAG: {e}")
230
+ rag_context_provider = None
231
+ else:
232
+ rag_context_provider = None
233
 
234
  def generate_response(message, history):
235
  """Generate response using OpenRouter API"""
 
259
  if ENABLE_DYNAMIC_URLS:
260
  urls_in_message = extract_urls_from_text(message)
261
  if urls_in_message:
262
+ # Fetch content from URLs mentioned in the message
263
  dynamic_context_parts = []
264
+ for url in urls_in_message[:3]: # Limit to 3 URLs per message
265
  content = fetch_url_content(url)
266
  dynamic_context_parts.append(f"\n\nDynamic context from {url}:\n{content}")
267
  if dynamic_context_parts:
 
270
  # Build enhanced system prompt with grounding context
271
  enhanced_system_prompt = SYSTEM_PROMPT + grounding_context
272
 
273
+ # Build messages array for the API
274
+ messages = [{"role": "system", "content": enhanced_system_prompt}]
 
 
 
275
 
276
+ # Add conversation history - handle both modern messages format and legacy tuples
277
  for chat in history:
278
  if isinstance(chat, dict):
279
+ # Modern format: {"role": "user", "content": "..."} or {"role": "assistant", "content": "..."}
280
  messages.append(chat)
281
  elif isinstance(chat, (list, tuple)) and len(chat) >= 2:
282
+ # Legacy format: ["user msg", "assistant msg"] or ("user msg", "assistant msg")
283
  user_msg, assistant_msg = chat[0], chat[1]
284
  if user_msg:
285
  messages.append({"role": "user", "content": user_msg})
 
289
  # Add current message
290
  messages.append({"role": "user", "content": message})
291
 
292
+ # Make API request with enhanced error handling
293
  try:
294
+ print(f"πŸ”„ Making API request to OpenRouter...")
295
+ print(f" Model: {MODEL}")
296
+ print(f" Messages: {len(messages)} in conversation")
297
+
298
  response = requests.post(
299
  url="https://openrouter.ai/api/v1/chat/completions",
300
  headers={
 
307
  "model": MODEL,
308
  "messages": messages,
309
  "temperature": 0.7,
310
+ "max_tokens": 1500
 
311
  },
312
  timeout=30
313
  )
314
 
315
+ print(f"πŸ“‘ API Response: {response.status_code}")
316
+
317
  if response.status_code == 200:
318
  try:
319
+ result = response.json()
320
+
321
+ # Enhanced validation of API response structure
322
+ if 'choices' not in result or not result['choices']:
323
+ print(f"⚠️ API response missing choices: {result}")
324
+ return "API Error: No response choices available"
325
+ elif 'message' not in result['choices'][0]:
326
+ print(f"⚠️ API response missing message: {result}")
327
+ return "API Error: No message in response"
328
+ elif 'content' not in result['choices'][0]['message']:
329
+ print(f"⚠️ API response missing content: {result}")
330
+ return "API Error: No content in message"
331
+ else:
332
+ content = result['choices'][0]['message']['content']
333
+
334
+ # Check for empty content
335
+ if not content or content.strip() == "":
336
+ print(f"⚠️ API returned empty content")
337
+ return "API Error: Empty response content"
338
+
339
+ print(f"βœ… API request successful")
340
+ return content
341
+
342
  except (KeyError, IndexError, json.JSONDecodeError) as e:
343
  print(f"❌ Failed to parse API response: {str(e)}")
344
  return f"API Error: Failed to parse response - {str(e)}"
 
355
  elif response.status_code == 429:
356
  error_msg = f"⏱️ **Rate Limit Exceeded**\n\n"
357
  error_msg += f"Too many requests. Please wait a moment and try again.\n\n"
358
+ error_msg += f"**Troubleshooting:**\n"
359
+ error_msg += f"1. Wait 30-60 seconds before trying again\n"
360
+ error_msg += f"2. Check your OpenRouter usage limits\n"
361
+ error_msg += f"3. Consider upgrading your OpenRouter plan"
362
  print(f"❌ Rate limit exceeded: {response.status_code}")
363
  return error_msg
364
+ elif response.status_code == 400:
365
+ try:
366
+ error_data = response.json()
367
+ error_message = error_data.get('error', {}).get('message', 'Unknown error')
368
+ except:
369
+ error_message = response.text
370
+
371
+ error_msg = f"⚠️ **Request Error**\n\n"
372
+ error_msg += f"The API request was invalid:\n"
373
+ error_msg += f"`{error_message}`\n\n"
374
+ if "model" in error_message.lower():
375
+ error_msg += f"**Model Issue:** The model `{MODEL}` may not be available.\n"
376
+ error_msg += f"Try switching to a different model in your Space configuration."
377
+ print(f"❌ Bad request: {response.status_code} - {error_message}")
378
+ return error_msg
379
  else:
380
  error_msg = f"🚫 **API Error {response.status_code}**\n\n"
381
  error_msg += f"An unexpected error occurred. Please try again.\n\n"
382
+ error_msg += f"If this persists, check:\n"
383
+ error_msg += f"1. OpenRouter service status\n"
384
+ error_msg += f"2. Your API key and credits\n"
385
+ error_msg += f"3. The model availability"
386
  print(f"❌ API error: {response.status_code} - {response.text[:200]}")
387
  return error_msg
388
 
389
  except requests.exceptions.Timeout:
390
  error_msg = f"⏰ **Request Timeout**\n\n"
391
+ error_msg += f"The API request took too long (30s limit).\n\n"
392
+ error_msg += f"**Troubleshooting:**\n"
393
+ error_msg += f"1. Try again with a shorter message\n"
394
+ error_msg += f"2. Check your internet connection\n"
395
+ error_msg += f"3. Try a different model"
396
+ print(f"❌ Request timeout after 30 seconds")
397
  return error_msg
398
  except requests.exceptions.ConnectionError:
399
  error_msg = f"🌐 **Connection Error**\n\n"
400
+ error_msg += f"Could not connect to OpenRouter API.\n\n"
401
+ error_msg += f"**Troubleshooting:**\n"
402
+ error_msg += f"1. Check your internet connection\n"
403
+ error_msg += f"2. Check OpenRouter service status\n"
404
+ error_msg += f"3. Try again in a few moments"
405
+ print(f"❌ Connection error to OpenRouter API")
406
  return error_msg
407
  except Exception as e:
408
+ error_msg = f"❌ **Unexpected Error**\n\n"
409
+ error_msg += f"An unexpected error occurred:\n"
410
+ error_msg += f"`{str(e)}`\n\n"
411
  error_msg += f"Please try again or contact support if this persists."
412
  print(f"❌ Unexpected error: {str(e)}")
413
  return error_msg
414
 
415
+ # Access code verification
416
  access_granted = gr.State(False)
417
+ _access_granted_global = False # Global fallback
418
 
419
  def verify_access_code(code):
420
  """Verify the access code"""
421
  global _access_granted_global
422
  if not ACCESS_CODE:
423
+ _access_granted_global = True
424
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=True)
425
 
426
+ if code == ACCESS_CODE:
427
  _access_granted_global = True
428
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=True)
429
  else:
430
+ _access_granted_global = False
431
+ return gr.update(visible=True, value="❌ Incorrect access code. Please try again."), gr.update(visible=False), gr.update(value=False)
432
 
433
  def protected_generate_response(message, history):
434
+ """Protected response function that checks access"""
435
+ # Check if access is granted via the global variable
436
  if ACCESS_CODE and not _access_granted_global:
437
+ return "Please enter the access code to continue."
438
  return generate_response(message, history)
439
 
440
+ # Global variable to store chat history for export
441
+ chat_history_store = []
442
+
443
  def store_and_generate_response(message, history):
444
+ """Wrapper function that stores history and generates response"""
445
  global chat_history_store
446
 
447
+ # Generate response using the protected function
448
  response = protected_generate_response(message, history)
449
 
450
+ # Convert current history to the format we need for export
451
+ # history comes in as [["user1", "bot1"], ["user2", "bot2"], ...]
452
+ chat_history_store = []
453
+ if history:
454
+ for exchange in history:
455
+ if isinstance(exchange, (list, tuple)) and len(exchange) >= 2:
456
+ chat_history_store.append({"role": "user", "content": exchange[0]})
457
+ chat_history_store.append({"role": "assistant", "content": exchange[1]})
458
+
459
+ # Add the current exchange
460
+ chat_history_store.append({"role": "user", "content": message})
461
+ chat_history_store.append({"role": "assistant", "content": response})
462
 
463
  return response
464
 
 
470
  markdown_content = export_conversation_to_markdown(chat_history_store)
471
 
472
  # Save to temporary file
473
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
474
  f.write(markdown_content)
475
  temp_file = f.name
476
 
477
  return gr.update(value=temp_file, visible=True)
478
 
479
  def export_conversation(history):
480
+ """Export conversation to markdown file"""
481
  if not history:
482
+ return gr.update(visible=False)
483
+
484
+ markdown_content = export_conversation_to_markdown(history)
485
 
486
+ # Save to temporary file
487
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
488
+ f.write(markdown_content)
489
+ temp_file = f.name
490
+
491
+ return gr.update(value=temp_file, visible=True)
492
 
493
+ # Configuration status display
494
  def get_configuration_status():
495
+ """Generate a configuration status message for display"""
496
  status_parts = []
497
 
498
  if API_KEY_VALID:
 
520
 
521
  return "\n".join(status_parts)
522
 
 
 
 
523
  # Create interface with access code protection
524
  with gr.Blocks(title=SPACE_NAME) as demo:
525
  gr.Markdown(f"# {SPACE_NAME}")
 
529
  with gr.Accordion("πŸ“Š Configuration Status", open=not API_KEY_VALID):
530
  gr.Markdown(get_configuration_status())
531
 
532
+ # Access code section (shown only if ACCESS_CODE is set)
533
+ with gr.Column(visible=bool(ACCESS_CODE)) as access_section:
534
+ gr.Markdown("### πŸ” Access Required")
535
+ gr.Markdown("Please enter the access code provided by your instructor:")
 
536
 
537
  access_input = gr.Textbox(
538
  label="Access Code",
 
577
  )
578
 
579
  if __name__ == "__main__":
580
+ demo.launch()
config.json CHANGED
@@ -1,15 +1,15 @@
1
  {
2
- "name": "Britannica Wiki Search",
3
  "description": "",
4
- "system_prompt": "You are a research aid specializing in academic literature search and analysis. Your expertise spans discovering peer-reviewed sources, assessing research methodologies, synthesizing findings across studies, and delivering properly formatted citations. When responding, anchor claims in specific sources from provided URL contexts, differentiate between direct evidence and interpretive analysis, and note any limitations or contradictory results. Employ clear, accessible language that demystifies complex research, and propose connected research directions when appropriate. Your purpose is to serve as an informed research tool supporting users through initial concept development, exploratory investigation, information collection, and source compilation.",
5
- "model": "openai/gpt-4o-mini-search-preview",
6
  "api_key_var": "OPENROUTER_API_KEY",
7
  "temperature": 0.7,
8
  "max_tokens": 1500,
9
- "examples": "[\"Teach me about the history of the beatniks\", \"Find commentary on internet discourse\"]",
10
- "grounding_urls": "[\"https://www.wikipedia.org/\", \"https://www.britannica.com/\"]",
11
  "access_code": "",
12
- "enable_dynamic_urls": true,
13
  "enable_vector_rag": false,
14
  "rag_data_json": "None"
15
  }
 
1
  {
2
+ "name": "Socratic Aid",
3
  "description": "",
4
+ "system_prompt": "You are a pedagogically-minded academic assistant designed for introductory courses. Your approach follows constructivist learning principles: build on students' prior knowledge, scaffold complex concepts through graduated questioning, and use Socratic dialogue to guide discovery. Provide concise, evidence-based explanations that connect theory to lived experiences. Each response should model critical thinking by acknowledging multiple perspectives, identifying assumptions, and revealing conceptual relationships. Conclude with open-ended questions that promote higher-order thinking\u2014analysis, synthesis, or evaluation\u2014rather than recall.",
5
+ "model": "google/gemini-2.0-flash-001",
6
  "api_key_var": "OPENROUTER_API_KEY",
7
  "temperature": 0.7,
8
  "max_tokens": 1500,
9
+ "examples": "[\"Hello! How can you help me?\", \"Tell me something interesting\", \"What can you do?\"]",
10
+ "grounding_urls": "[]",
11
  "access_code": "",
12
+ "enable_dynamic_urls": false,
13
  "enable_vector_rag": false,
14
  "rag_data_json": "None"
15
  }