milwright commited on
Commit
63d8d8e
Β·
verified Β·
1 Parent(s): be8b3db

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +108 -211
app.py CHANGED
@@ -33,21 +33,21 @@ def validate_api_key():
33
  """Validate API key configuration with detailed logging"""
34
  if not API_KEY:
35
  print(f"⚠️ API KEY CONFIGURATION ERROR:")
36
- print(f" Variable name: {api_key_var}")
37
  print(f" Status: Not set or empty")
38
- print(f" Action needed: Set '{api_key_var}' in HuggingFace Space secrets")
39
  print(f" Expected format: sk-or-xxxxxxxxxx")
40
  return False
41
  elif not API_KEY.startswith('sk-or-'):
42
  print(f"⚠️ API KEY FORMAT WARNING:")
43
- print(f" Variable name: {api_key_var}")
44
  print(f" Current value: {API_KEY[:10]}..." if len(API_KEY) > 10 else API_KEY)
45
  print(f" Expected format: sk-or-xxxxxxxxxx")
46
  print(f" Note: OpenRouter keys should start with 'sk-or-'")
47
  return True # Still try to use it
48
  else:
49
  print(f"βœ… API Key configured successfully")
50
- print(f" Variable: {api_key_var}")
51
  print(f" Format: Valid OpenRouter key")
52
  return True
53
 
@@ -63,79 +63,63 @@ def validate_url_domain(url):
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,62 +158,41 @@ def export_conversation_to_markdown(conversation_history):
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"""
@@ -240,10 +203,10 @@ def generate_response(message, history):
240
  error_msg += f"Please configure your OpenRouter API key:\n"
241
  error_msg += f"1. Go to Settings (βš™οΈ) in your HuggingFace Space\n"
242
  error_msg += f"2. Click 'Variables and secrets'\n"
243
- error_msg += f"3. Add secret: **{api_key_var}**\n"
244
  error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\n\n"
245
  error_msg += f"Get your API key at: https://openrouter.ai/keys"
246
- print(f"❌ API request failed: No API key configured for {api_key_var}")
247
  return error_msg
248
 
249
  # Get grounding context
@@ -259,9 +222,8 @@ def generate_response(message, history):
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,16 +232,19 @@ def generate_response(message, history):
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,12 +254,8 @@ def generate_response(message, history):
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,38 +268,18 @@ def generate_response(message, history):
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)}"
@@ -346,7 +287,7 @@ def generate_response(message, history):
346
  error_msg = f"πŸ” **Authentication Error**\n\n"
347
  error_msg += f"Your API key appears to be invalid or expired.\n\n"
348
  error_msg += f"**Troubleshooting:**\n"
349
- error_msg += f"1. Check that your **{OPENROUTER_API_KEY}** is set correctly\n"
350
  error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\n"
351
  error_msg += f"3. Ensure your key starts with `sk-or-`\n"
352
  error_msg += f"4. Check that you have credits on your OpenRouter account"
@@ -355,104 +296,64 @@ def generate_response(message, history):
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
- # Store the updated history
448
- chat_history_store = history.copy() if history else []
449
-
450
- # Generate response using the protected function
451
  response = protected_generate_response(message, history)
452
 
453
- # Update stored history with the new exchange
454
- chat_history_store.append({"role": "user", "content": message})
455
- chat_history_store.append({"role": "assistant", "content": response})
456
 
457
  return response
458
 
@@ -464,35 +365,27 @@ def export_current_conversation():
464
  markdown_content = export_conversation_to_markdown(chat_history_store)
465
 
466
  # Save to temporary file
467
- with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
468
  f.write(markdown_content)
469
  temp_file = f.name
470
 
471
  return gr.update(value=temp_file, visible=True)
472
 
473
  def export_conversation(history):
474
- """Export conversation to markdown file"""
475
  if not history:
476
- return gr.update(visible=False)
477
-
478
- markdown_content = export_conversation_to_markdown(history)
479
-
480
- # Save to temporary file
481
- with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
482
- f.write(markdown_content)
483
- temp_file = f.name
484
 
485
- return gr.update(value=temp_file, visible=True)
486
 
487
- # Configuration status display
488
  def get_configuration_status():
489
- """Generate a configuration status message for display"""
490
  status_parts = []
491
 
492
  if API_KEY_VALID:
493
  status_parts.append("βœ… **API Key:** Configured and valid")
494
  else:
495
- status_parts.append("❌ **API Key:** Not configured - Set `{OPENROUTER_API_KEY}` in Space secrets")
496
 
497
  status_parts.append(f"πŸ€– **Model:** {MODEL}")
498
  status_parts.append(f"🌑️ **Temperature:** 0.7")
@@ -514,6 +407,9 @@ def get_configuration_status():
514
 
515
  return "\n".join(status_parts)
516
 
 
 
 
517
  # Create interface with access code protection
518
  with gr.Blocks(title=SPACE_NAME) as demo:
519
  gr.Markdown(f"# {SPACE_NAME}")
@@ -523,10 +419,11 @@ with gr.Blocks(title=SPACE_NAME) as demo:
523
  with gr.Accordion("πŸ“Š Configuration Status", open=not API_KEY_VALID):
524
  gr.Markdown(get_configuration_status())
525
 
526
- # Access code section (shown only if ACCESS_CODE is set)
527
- with gr.Column(visible=bool(ACCESS_CODE)) as access_section:
528
- gr.Markdown("### πŸ” Access Required")
529
- gr.Markdown("Please enter the access code provided by your instructor:")
 
530
 
531
  access_input = gr.Textbox(
532
  label="Access Code",
@@ -571,4 +468,4 @@ with gr.Blocks(title=SPACE_NAME) as demo:
571
  )
572
 
573
  if __name__ == "__main__":
574
- demo.launch()
 
33
  """Validate API key configuration with detailed logging"""
34
  if not API_KEY:
35
  print(f"⚠️ API KEY CONFIGURATION ERROR:")
36
+ print(f" Variable name: OPENROUTER_API_KEY")
37
  print(f" Status: Not set or empty")
38
+ print(f" Action needed: Set 'OPENROUTER_API_KEY' in HuggingFace Space secrets")
39
  print(f" Expected format: sk-or-xxxxxxxxxx")
40
  return False
41
  elif not API_KEY.startswith('sk-or-'):
42
  print(f"⚠️ API KEY FORMAT WARNING:")
43
+ print(f" Variable name: OPENROUTER_API_KEY")
44
  print(f" Current value: {API_KEY[:10]}..." if len(API_KEY) > 10 else API_KEY)
45
  print(f" Expected format: sk-or-xxxxxxxxxx")
46
  print(f" Note: OpenRouter keys should start with 'sk-or-'")
47
  return True # Still try to use it
48
  else:
49
  print(f"βœ… API Key configured successfully")
50
+ print(f" Variable: OPENROUTER_API_KEY")
51
  print(f" Format: Valid OpenRouter key")
52
  return True
53
 
 
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
 
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"""
 
203
  error_msg += f"Please configure your OpenRouter API key:\n"
204
  error_msg += f"1. Go to Settings (βš™οΈ) in your HuggingFace Space\n"
205
  error_msg += f"2. Click 'Variables and secrets'\n"
206
+ error_msg += f"3. Add secret: **OPENROUTER_API_KEY**\n"
207
  error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\n\n"
208
  error_msg += f"Get your API key at: https://openrouter.ai/keys"
209
+ print(f"❌ API request failed: No API key configured for OPENROUTER_API_KEY")
210
  return error_msg
211
 
212
  # Get grounding context
 
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
  # 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
  # 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
  "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)}"
 
287
  error_msg = f"πŸ” **Authentication Error**\n\n"
288
  error_msg += f"Your API key appears to be invalid or expired.\n\n"
289
  error_msg += f"**Troubleshooting:**\n"
290
+ error_msg += f"1. Check that your **OPENROUTER_API_KEY** secret is set correctly\n"
291
  error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\n"
292
  error_msg += f"3. Ensure your key starts with `sk-or-`\n"
293
  error_msg += f"4. Check that you have credits on your OpenRouter account"
 
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
  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:
386
  status_parts.append("βœ… **API Key:** Configured and valid")
387
  else:
388
+ status_parts.append("❌ **API Key:** Not configured - Set `OPENROUTER_API_KEY` in Space secrets")
389
 
390
  status_parts.append(f"πŸ€– **Model:** {MODEL}")
391
  status_parts.append(f"🌑️ **Temperature:** 0.7")
 
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
  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
  )
469
 
470
  if __name__ == "__main__":
471
+ demo.launch()