milwright commited on
Commit
12f209c
Β·
verified Β·
1 Parent(s): c4564a6

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +234 -206
  2. config.json +5 -3
app.py CHANGED
@@ -10,39 +10,68 @@ import urllib.parse
10
 
11
 
12
  # Configuration
13
- SPACE_NAME = "AI Assistant"
14
- SPACE_DESCRIPTION = "A customizable AI assistant"
15
 
16
- # Default configuration values
17
- DEFAULT_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."""
18
- DEFAULT_TEMPERATURE = 0.7
19
- DEFAULT_MAX_TOKENS = 750
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- # Try to load configuration from file (if modified by faculty)
22
- try:
23
- with open('config.json', 'r') as f:
24
- saved_config = json.load(f)
25
- SYSTEM_PROMPT = saved_config.get('system_prompt', DEFAULT_SYSTEM_PROMPT)
26
- temperature = saved_config.get('temperature', DEFAULT_TEMPERATURE)
27
- max_tokens = saved_config.get('max_tokens', DEFAULT_MAX_TOKENS)
28
- print("βœ… Loaded configuration from config.json")
29
- except:
30
- # Use defaults if no config file or error
31
- SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT
32
- temperature = DEFAULT_TEMPERATURE
33
- max_tokens = DEFAULT_MAX_TOKENS
34
- print("ℹ️ Using default configuration")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- MODEL = "google/gemini-2.0-flash-001"
37
- THEME = "Default" # Gradio theme name
38
- GROUNDING_URLS = []
39
  # Get access code from environment variable for security
40
  # If ACCESS_CODE is not set, no access control is applied
41
  ACCESS_CODE = os.environ.get("ACCESS_CODE")
42
- ENABLE_DYNAMIC_URLS = True
43
 
44
  # Get API key from environment - customizable variable name with validation
45
- API_KEY = os.environ.get("API_KEY")
 
46
  if API_KEY:
47
  API_KEY = API_KEY.strip() # Remove any whitespace
48
  if not API_KEY: # Check if empty after stripping
@@ -53,21 +82,21 @@ def validate_api_key():
53
  """Validate API key configuration with detailed logging"""
54
  if not API_KEY:
55
  print(f"⚠️ API KEY CONFIGURATION ERROR:")
56
- print(f" Variable name: API_KEY")
57
  print(f" Status: Not set or empty")
58
- print(f" Action needed: Set 'API_KEY' in HuggingFace Space secrets")
59
  print(f" Expected format: sk-or-xxxxxxxxxx")
60
  return False
61
  elif not API_KEY.startswith('sk-or-'):
62
  print(f"⚠️ API KEY FORMAT WARNING:")
63
- print(f" Variable name: API_KEY")
64
- print(f" Current value: {API_KEY[:10]}..." if len(API_KEY) > 10 else API_KEY)
65
  print(f" Expected format: sk-or-xxxxxxxxxx")
66
  print(f" Note: OpenRouter keys should start with 'sk-or-'")
67
  return True # Still try to use it
68
  else:
69
  print(f"βœ… API Key configured successfully")
70
- print(f" Variable: API_KEY")
71
  print(f" Format: Valid OpenRouter key")
72
  return True
73
 
@@ -181,7 +210,10 @@ def get_grounding_context():
181
  context_parts.append(f"[{priority_label}] Context from URL {i} ({url}):\n{content}")
182
 
183
  if context_parts:
184
- result = "\n\n" + "\n\n".join(context_parts) + "\n\n"
 
 
 
185
  else:
186
  result = ""
187
 
@@ -195,7 +227,7 @@ def export_conversation_to_markdown(conversation_history):
195
  return "No conversation to export."
196
 
197
  markdown_content = f"""# Conversation Export
198
- Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
199
 
200
  ---
201
 
@@ -209,17 +241,21 @@ Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
209
 
210
  if role == 'user':
211
  message_pair_count += 1
212
- markdown_content += f"## User Message {message_pair_count}\n\n{content}\n\n"
 
213
  elif role == 'assistant':
214
- markdown_content += f"## Assistant Response {message_pair_count}\n\n{content}\n\n---\n\n"
 
215
  elif isinstance(message, (list, tuple)) and len(message) >= 2:
216
  # Handle legacy tuple format: ["user msg", "assistant msg"]
217
  message_pair_count += 1
218
  user_msg, assistant_msg = message[0], message[1]
219
  if user_msg:
220
- markdown_content += f"## User Message {message_pair_count}\n\n{user_msg}\n\n"
 
221
  if assistant_msg:
222
- markdown_content += f"## Assistant Response {message_pair_count}\n\n{assistant_msg}\n\n---\n\n"
 
223
 
224
  return markdown_content
225
 
@@ -229,14 +265,20 @@ def generate_response(message, history):
229
 
230
  # Enhanced API key validation with helpful messages
231
  if not API_KEY:
232
- error_msg = f"πŸ”‘ **API Key Required**\n\n"
233
- error_msg += f"Please configure your OpenRouter API key:\n"
234
- error_msg += f"1. Go to Settings (βš™οΈ) in your HuggingFace Space\n"
235
- error_msg += f"2. Click 'Variables and secrets'\n"
236
- error_msg += f"3. Add secret: **API_KEY**\n"
237
- error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\n\n"
 
 
 
 
 
 
238
  error_msg += f"Get your API key at: https://openrouter.ai/keys"
239
- print(f"❌ API request failed: No API key configured for API_KEY")
240
  return error_msg
241
 
242
  # Get grounding context
@@ -253,7 +295,8 @@ def generate_response(message, history):
253
  content = fetch_url_content(url)
254
  dynamic_context_parts.append(f"\n\nDynamic context from {url}:\n{content}")
255
  if dynamic_context_parts:
256
- grounding_context += "\n".join(dynamic_context_parts)
 
257
 
258
  # Build enhanced system prompt with grounding context
259
  enhanced_system_prompt = SYSTEM_PROMPT + grounding_context
@@ -331,19 +374,28 @@ def generate_response(message, history):
331
  print(f"❌ Failed to parse API response: {str(e)}")
332
  return f"API Error: Failed to parse response - {str(e)}"
333
  elif response.status_code == 401:
334
- error_msg = f"πŸ” **Authentication Error**\n\n"
335
- error_msg += f"Your API key appears to be invalid or expired.\n\n"
336
- error_msg += f"**Troubleshooting:**\n"
337
- error_msg += f"1. Check that your **API_KEY** secret is set correctly\n"
338
- error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\n"
339
- error_msg += f"3. Ensure your key starts with `sk-or-`\n"
 
 
 
 
 
 
340
  error_msg += f"4. Check that you have credits on your OpenRouter account"
341
  print(f"❌ API authentication failed: {response.status_code} - {response.text[:200]}")
342
  return error_msg
343
  elif response.status_code == 429:
344
- error_msg = f"⏱️ **Rate Limit Exceeded**\n\n"
345
- error_msg += f"Too many requests. Please wait a moment and try again.\n\n"
346
- error_msg += f"**Troubleshooting:**\n"
 
 
 
347
  error_msg += f"1. Wait 30-60 seconds before trying again\n"
348
  error_msg += f"2. Check your OpenRouter usage limits\n"
349
  error_msg += f"3. Consider upgrading your OpenRouter plan"
@@ -356,17 +408,21 @@ def generate_response(message, history):
356
  except:
357
  error_message = response.text
358
 
359
- error_msg = f"⚠️ **Request Error**\n\n"
 
360
  error_msg += f"The API request was invalid:\n"
361
- error_msg += f"`{error_message}`\n\n"
 
362
  if "model" in error_message.lower():
363
  error_msg += f"**Model Issue:** The model `{MODEL}` may not be available.\n"
364
  error_msg += f"Try switching to a different model in your Space configuration."
365
  print(f"❌ Bad request: {response.status_code} - {error_message}")
366
  return error_msg
367
  else:
368
- error_msg = f"🚫 **API Error {response.status_code}**\n\n"
369
- error_msg += f"An unexpected error occurred. Please try again.\n\n"
 
 
370
  error_msg += f"If this persists, check:\n"
371
  error_msg += f"1. OpenRouter service status\n"
372
  error_msg += f"2. Your API key and credits\n"
@@ -375,8 +431,10 @@ def generate_response(message, history):
375
  return error_msg
376
 
377
  except requests.exceptions.Timeout:
378
- error_msg = f"⏰ **Request Timeout**\n\n"
379
- error_msg += f"The API request took too long (30s limit).\n\n"
 
 
380
  error_msg += f"**Troubleshooting:**\n"
381
  error_msg += f"1. Try again with a shorter message\n"
382
  error_msg += f"2. Check your internet connection\n"
@@ -384,8 +442,10 @@ def generate_response(message, history):
384
  print(f"❌ Request timeout after 30 seconds")
385
  return error_msg
386
  except requests.exceptions.ConnectionError:
387
- error_msg = f"🌐 **Connection Error**\n\n"
388
- error_msg += f"Could not connect to OpenRouter API.\n\n"
 
 
389
  error_msg += f"**Troubleshooting:**\n"
390
  error_msg += f"1. Check your internet connection\n"
391
  error_msg += f"2. Check OpenRouter service status\n"
@@ -393,10 +453,12 @@ def generate_response(message, history):
393
  print(f"❌ Connection error to OpenRouter API")
394
  return error_msg
395
  except Exception as e:
396
- error_msg = f"❌ **Unexpected Error**\n\n"
397
- error_msg += f"An unexpected error occurred:\n"
398
- error_msg += f"`{str(e)}`\n\n"
399
- error_msg += f"Please try again or contact support if this persists."
 
 
400
  print(f"❌ Unexpected error: {str(e)}")
401
  return error_msg
402
 
@@ -480,46 +542,53 @@ def export_conversation(history):
480
 
481
  # Configuration status display
482
  def get_configuration_status():
483
- """Generate a configuration status message for display"""
484
  status_parts = []
485
 
486
- # API Key status
487
- status_parts.append("### πŸ”‘ API Configuration")
488
- if API_KEY_VALID:
489
- status_parts.append("βœ… **API Key:** Ready")
490
- else:
491
- status_parts.append("❌ **API Key:** Not configured")
492
- status_parts.append(" Set `API_KEY` in Space secrets")
493
-
494
- # Model and parameters
495
- status_parts.append("") # Blank line
496
- status_parts.append("### πŸ€– Model Settings")
497
- status_parts.append(f"**Model:** {MODEL.split('/')[-1]}")
498
  status_parts.append(f"**Temperature:** 0.7")
499
- status_parts.append(f"**Max Tokens:** 750")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
 
501
  # URL Context if configured
502
- if GROUNDING_URLS:
503
- status_parts.append("") # Blank line
504
- status_parts.append("### πŸ”— Context Sources")
505
- status_parts.append(f"**URLs Configured:** {len(GROUNDING_URLS)}")
506
- for i, url in enumerate(GROUNDING_URLS[:2], 1):
507
- status_parts.append(f" {i}. {url[:50]}{'...' if len(url) > 50 else ''}")
508
- if len(GROUNDING_URLS) > 2:
509
- status_parts.append(f" ... and {len(GROUNDING_URLS) - 2} more")
510
-
511
- # Access control
512
- if ACCESS_CODE is not None:
513
- status_parts.append("") # Blank line
514
- status_parts.append("### πŸ” Access Control")
515
- status_parts.append("**Status:** Password protected")
516
-
517
- # System prompt
518
- status_parts.append("") # Blank line
519
- status_parts.append("### πŸ“ System Prompt")
520
- # Show first 200 chars of system prompt
521
- prompt_preview = SYSTEM_PROMPT[:200] + "..." if len(SYSTEM_PROMPT) > 200 else SYSTEM_PROMPT
522
- status_parts.append(f"```\n{prompt_preview}\n```")
523
 
524
  return "\n".join(status_parts)
525
 
@@ -545,11 +614,20 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
545
 
546
  # Main chat interface (hidden until access granted)
547
  with gr.Column(visible=(ACCESS_CODE is None)) as chat_section:
 
 
 
 
 
 
 
 
 
548
  chat_interface = gr.ChatInterface(
549
  fn=store_and_generate_response, # Use wrapper function to store history
550
  title="", # Title already shown above
551
  description="", # Description already shown above
552
- examples=['Hello! How can you help me?', 'Tell me something interesting', 'What can you do?'],
553
  type="messages" # Use modern message format for better compatibility
554
  )
555
 
@@ -564,6 +642,9 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
564
  outputs=[export_file]
565
  )
566
 
 
 
 
567
 
568
  # Connect access verification
569
  if ACCESS_CODE is not None:
@@ -610,29 +691,11 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
610
  with open('config.json', 'r') as f:
611
  current_config = json.load(f)
612
  except:
613
- current_config = {
614
- 'system_prompt': SYSTEM_PROMPT,
615
- 'temperature': 0.7,
616
- 'max_tokens': 750,
617
- 'locked': False
618
- }
619
-
620
- # Editable fields - Order matches the Configuration tab
621
- # 1. Assistant Identity
622
- edit_name = gr.Textbox(
623
- label="Assistant Name",
624
- value=current_config.get('name', SPACE_NAME),
625
- placeholder="My AI Assistant"
626
- )
627
-
628
- edit_description = gr.Textbox(
629
- label="Assistant Description",
630
- value=current_config.get('description', SPACE_DESCRIPTION),
631
- lines=2,
632
- placeholder="A helpful AI assistant for..."
633
- )
634
 
635
- # 2. System Prompt
 
636
  edit_system_prompt = gr.Textbox(
637
  label="System Prompt",
638
  value=current_config.get('system_prompt', SYSTEM_PROMPT),
@@ -654,8 +717,8 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
654
  ],
655
  value=current_config.get('model', MODEL)
656
  )
657
-
658
- # 4. Example Prompts
659
  examples_value = current_config.get('examples', [])
660
  if isinstance(examples_value, list):
661
  examples_text_value = "\n".join(examples_value)
@@ -686,7 +749,7 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
686
  step=50
687
  )
688
 
689
- # 6. URL Grounding
690
  gr.Markdown("### URL Grounding")
691
  grounding_urls_value = current_config.get('grounding_urls', [])
692
  if isinstance(grounding_urls_value, str):
@@ -713,8 +776,8 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
713
  )
714
 
715
  with gr.Row():
716
- save_config_btn = gr.Button("πŸ’Ύ Save Configuration", variant="primary")
717
- reset_config_btn = gr.Button("↩️ Reset to Defaults", variant="secondary")
718
 
719
  config_status = gr.Markdown("")
720
 
@@ -722,39 +785,30 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
722
  def verify_faculty_password(password):
723
  if password == FACULTY_PASSWORD:
724
  return (
725
- gr.update(value="βœ… Authentication successful!"),
726
  gr.update(visible=False), # Hide auth row
727
  gr.update(visible=True), # Show config section
728
  True # Update auth state
729
  )
730
  else:
731
  return (
732
- gr.update(value="❌ Invalid password"),
733
  gr.update(visible=True), # Keep auth row visible
734
  gr.update(visible=False), # Keep config hidden
735
  False # Auth failed
736
  )
737
 
738
  # Save configuration function
739
- def save_configuration(new_name, new_description, new_prompt, new_model, new_examples, new_temp, new_tokens, *args):
740
- # Extract URL values, lock_config, and is_authenticated from args
741
- # args should contain: url1, url2, ..., url10, lock_config, is_authenticated
742
- if len(args) < 12: # Need at least 10 URLs + lock_config + is_authenticated
743
- return "❌ Invalid number of parameters"
744
-
745
- url_values = args[:10] # First 10 are URLs
746
- lock_config = args[10] # 11th is lock_config
747
- is_authenticated = args[11] # 12th is is_authenticated
748
-
749
  if not is_authenticated:
750
- return "❌ Not authenticated"
751
 
752
  # Check if configuration is already locked
753
  try:
754
  with open('config.json', 'r') as f:
755
  existing_config = json.load(f)
756
  if existing_config.get('locked', False):
757
- return "πŸ”’ Configuration is locked and cannot be modified"
758
  except:
759
  pass
760
 
@@ -763,26 +817,27 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
763
  with open('config.json', 'r') as f:
764
  current_full_config = json.load(f)
765
  except:
766
- # If config.json doesn't exist, use global config
767
- current_full_config = config.copy()
768
 
769
  # Process example prompts
770
  examples_list = [ex.strip() for ex in new_examples.split('\n') if ex.strip()]
771
 
772
- # Process URL values
773
- grounding_urls = [url.strip() for url in url_values if url and url.strip()]
 
 
 
774
 
775
  # Update all editable fields while preserving everything else
776
  current_full_config.update({
777
- 'name': new_name,
778
- 'description': new_description,
779
  'system_prompt': new_prompt,
780
  'model': new_model,
781
  'examples': examples_list,
782
  'temperature': new_temp,
783
  'max_tokens': int(new_tokens),
784
  'grounding_urls': grounding_urls,
785
- 'locked': lock_config,
786
  'last_modified': datetime.now().isoformat(),
787
  'last_modified_by': 'faculty'
788
  })
@@ -791,61 +846,34 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
791
  with open('config.json', 'w') as f:
792
  json.dump(current_full_config, f, indent=2)
793
 
794
- # Reload all configuration values from the saved config
795
- global SPACE_NAME, SPACE_DESCRIPTION, SYSTEM_PROMPT, MODEL, temperature, max_tokens, GROUNDING_URLS
796
- SPACE_NAME = current_full_config.get('name', SPACE_NAME)
797
- SPACE_DESCRIPTION = current_full_config.get('description', SPACE_DESCRIPTION)
798
- SYSTEM_PROMPT = current_full_config.get('system_prompt', SYSTEM_PROMPT)
799
- MODEL = current_full_config.get('model', MODEL)
800
- temperature = current_full_config.get('temperature', temperature)
801
- max_tokens = current_full_config.get('max_tokens', max_tokens)
802
- GROUNDING_URLS = current_full_config.get('grounding_urls', GROUNDING_URLS)
803
 
804
- # Determine which changes require restart
805
- restart_required = False
806
- restart_reasons = []
807
-
808
- # Check if theme changed (requires restart)
809
- if 'theme' in current_full_config and current_full_config.get('theme') != THEME:
810
- restart_required = True
811
- restart_reasons.append("Theme change")
812
-
813
- # Check if name/description changed (requires restart for interface title)
814
- if new_name != SPACE_NAME or new_description != SPACE_DESCRIPTION:
815
- restart_required = True
816
- restart_reasons.append("Assistant name or description")
817
-
818
- # Check if examples changed (requires restart for ChatInterface)
819
- old_examples = config.get('examples', [])
820
- if examples_list != old_examples:
821
- restart_required = True
822
- restart_reasons.append("Example prompts")
823
-
824
- # Build response message
825
- response_msg = f"βœ… Configuration saved successfully at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
826
-
827
- # Add restart instructions if needed
828
- if restart_required:
829
- response_msg += f"\n\nπŸ”„ **Restart Required**\n"
830
- response_msg += f"The following changes require a Space restart to take effect:\n"
831
- for reason in restart_reasons:
832
- response_msg += f"β€’ {reason}\n"
833
- response_msg += "\n**To restart your Space:**\n"
834
- response_msg += "1. Go to your Space settings (βš™οΈ icon)\n"
835
- response_msg += "2. Click 'Factory reboot' for a complete restart\n"
836
- response_msg += "3. Wait ~30 seconds for the Space to reload\n"
837
- response_msg += "\n*Note: System prompt, model, temperature, and URL changes take effect immediately for new conversations.*"
838
  else:
839
- response_msg += "\n\nβœ… All changes will take effect immediately for new conversations."
840
-
841
- return response_msg
842
  except Exception as e:
843
  return f"❌ Error saving configuration: {str(e)}"
844
 
845
  # Reset configuration function
846
  def reset_configuration(is_authenticated):
847
  if not is_authenticated:
848
- updates = ["❌ Not authenticated"] + [gr.update() for _ in range(17)] # 1 status + 7 main fields + 10 URLs
849
  return tuple(updates)
850
 
851
  # Check if locked
@@ -853,7 +881,7 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
853
  with open('config.json', 'r') as f:
854
  existing_config = json.load(f)
855
  if existing_config.get('locked', False):
856
- updates = ["πŸ”’ Configuration is locked"] + [gr.update() for _ in range(17)]
857
  return tuple(updates)
858
  except:
859
  pass
@@ -865,20 +893,20 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
865
  else:
866
  examples_text = ""
867
 
868
- # Get default URLs
869
  default_urls = DEFAULT_CONFIG.get('grounding_urls', [])
870
  if isinstance(default_urls, str):
871
  try:
872
- import ast
873
- default_urls = ast.literal_eval(default_urls)
874
  except:
875
  default_urls = []
 
 
876
 
877
  # Reset to original default values
878
  updates = [
879
- "βœ… Reset to default values",
880
- gr.update(value=DEFAULT_CONFIG.get('name', SPACE_NAME)),
881
- gr.update(value=DEFAULT_CONFIG.get('description', SPACE_DESCRIPTION)),
882
  gr.update(value=DEFAULT_CONFIG.get('system_prompt', SYSTEM_PROMPT)),
883
  gr.update(value=DEFAULT_CONFIG.get('model', MODEL)),
884
  gr.update(value=examples_text),
@@ -909,17 +937,17 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
909
  # Connect configuration buttons
910
  save_config_btn.click(
911
  save_configuration,
912
- inputs=[edit_name, edit_description, edit_system_prompt, edit_model, edit_examples, edit_temperature, edit_max_tokens] + url_fields + [config_locked, faculty_auth_state],
913
  outputs=[config_status]
914
  )
915
 
916
  reset_config_btn.click(
917
  reset_configuration,
918
  inputs=[faculty_auth_state],
919
- outputs=[config_status, edit_name, edit_description, edit_system_prompt, edit_model, edit_examples, edit_temperature, edit_max_tokens] + url_fields
920
  )
921
  else:
922
- gr.Markdown("ℹ️ Faculty configuration is not enabled. Set CONFIG_CODE in Space secrets to enable.")
923
 
924
  if __name__ == "__main__":
925
  demo.launch()
 
10
 
11
 
12
  # Configuration
13
+ SPACE_NAME = 'AI Assistant'
14
+ SPACE_DESCRIPTION = 'A customizable AI assistant'
15
 
16
+ # Default configuration values (used only if config.json is missing)
17
+ DEFAULT_CONFIG = {{
18
+ 'name': SPACE_NAME,
19
+ 'description': SPACE_DESCRIPTION,
20
+ '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.",
21
+ 'temperature': 0.7,
22
+ 'max_tokens': 750,
23
+ 'model': 'google/gemini-2.0-flash-001',
24
+ 'api_key_var': 'API_KEY',
25
+ 'theme': 'Default',
26
+ 'grounding_urls': '[]',
27
+ 'enable_dynamic_urls': True,
28
+ 'examples': ['Can you help me understand why the sky is blue?'],
29
+ 'locked': False
30
+ }}
31
 
32
+ # Load configuration from file - this is the single source of truth
33
+ def load_config():
34
+ """Load configuration from config.json with fallback to defaults"""
35
+ try:
36
+ with open('config.json', 'r') as f:
37
+ config = json.load(f)
38
+ print("βœ… Loaded configuration from config.json")
39
+ return config
40
+ except FileNotFoundError:
41
+ print("ℹ️ No config.json found, using default configuration")
42
+ # Save default config for future use
43
+ try:
44
+ with open('config.json', 'w') as f:
45
+ json.dump(DEFAULT_CONFIG, f, indent=2)
46
+ print("βœ… Created config.json with default values")
47
+ except:
48
+ pass
49
+ return DEFAULT_CONFIG
50
+ except Exception as e:
51
+ print(f"⚠️ Error loading config.json: {{e}}, using defaults")
52
+ return DEFAULT_CONFIG
53
+
54
+ # Load configuration
55
+ config = load_config()
56
+
57
+ # Initial load of configuration values
58
+ SPACE_NAME = config.get('name', DEFAULT_CONFIG['name'])
59
+ SPACE_DESCRIPTION = config.get('description', DEFAULT_CONFIG['description'])
60
+ SYSTEM_PROMPT = config.get('system_prompt', DEFAULT_CONFIG['system_prompt'])
61
+ temperature = config.get('temperature', DEFAULT_CONFIG['temperature'])
62
+ max_tokens = config.get('max_tokens', DEFAULT_CONFIG['max_tokens'])
63
+ MODEL = config.get('model', DEFAULT_CONFIG['model'])
64
+ THEME = config.get('theme', DEFAULT_CONFIG['theme'])
65
+ GROUNDING_URLS = config.get('grounding_urls', DEFAULT_CONFIG['grounding_urls'])
66
+ ENABLE_DYNAMIC_URLS = config.get('enable_dynamic_urls', DEFAULT_CONFIG['enable_dynamic_urls'])
67
 
 
 
 
68
  # Get access code from environment variable for security
69
  # If ACCESS_CODE is not set, no access control is applied
70
  ACCESS_CODE = os.environ.get("ACCESS_CODE")
 
71
 
72
  # Get API key from environment - customizable variable name with validation
73
+ API_KEY_VAR = config.get('api_key_var', DEFAULT_CONFIG['api_key_var'])
74
+ API_KEY = os.environ.get(API_KEY_VAR)
75
  if API_KEY:
76
  API_KEY = API_KEY.strip() # Remove any whitespace
77
  if not API_KEY: # Check if empty after stripping
 
82
  """Validate API key configuration with detailed logging"""
83
  if not API_KEY:
84
  print(f"⚠️ API KEY CONFIGURATION ERROR:")
85
+ print(f" Variable name: {API_KEY_VAR}")
86
  print(f" Status: Not set or empty")
87
+ print(f" Action needed: Set '{API_KEY_VAR}' in HuggingFace Space secrets")
88
  print(f" Expected format: sk-or-xxxxxxxxxx")
89
  return False
90
  elif not API_KEY.startswith('sk-or-'):
91
  print(f"⚠️ API KEY FORMAT WARNING:")
92
+ print(f" Variable name: {API_KEY_VAR}")
93
+ print(f" Current value: {API_KEY[:10]}..." if len(API_KEY) > 10 else "{API_KEY}")
94
  print(f" Expected format: sk-or-xxxxxxxxxx")
95
  print(f" Note: OpenRouter keys should start with 'sk-or-'")
96
  return True # Still try to use it
97
  else:
98
  print(f"βœ… API Key configured successfully")
99
+ print(f" Variable: {API_KEY_VAR}")
100
  print(f" Format: Valid OpenRouter key")
101
  return True
102
 
 
210
  context_parts.append(f"[{priority_label}] Context from URL {i} ({url}):\n{content}")
211
 
212
  if context_parts:
213
+ result = "\n\
214
+ " + "\n\
215
+ ".join(context_parts) + "\n\
216
+ "
217
  else:
218
  result = ""
219
 
 
227
  return "No conversation to export."
228
 
229
  markdown_content = f"""# Conversation Export
230
+ Generated on: {datetime.now().strftime('%%Y-%%m-%%d %%H:%%M:%%S')}
231
 
232
  ---
233
 
 
241
 
242
  if role == 'user':
243
  message_pair_count += 1
244
+ markdown_content += f"## User Message {{message_pair_count}}\n\n{{content}}\n\
245
+ "
246
  elif role == 'assistant':
247
+ markdown_content += f"## Assistant Response {{message_pair_count}}\n\n{{content}}\n\n---\n\
248
+ "
249
  elif isinstance(message, (list, tuple)) and len(message) >= 2:
250
  # Handle legacy tuple format: ["user msg", "assistant msg"]
251
  message_pair_count += 1
252
  user_msg, assistant_msg = message[0], message[1]
253
  if user_msg:
254
+ markdown_content += f"## User Message {{message_pair_count}}\n\n{{user_msg}}\n\
255
+ "
256
  if assistant_msg:
257
+ markdown_content += f"## Assistant Response {{message_pair_count}}\n\n{{assistant_msg}}\n\n---\n\
258
+ "
259
 
260
  return markdown_content
261
 
 
265
 
266
  # Enhanced API key validation with helpful messages
267
  if not API_KEY:
268
+ error_msg = f"πŸ”‘ **API Key Required**\n\
269
+ "
270
+ error_msg += f"Please configure your OpenRouter API key:\
271
+ "
272
+ error_msg += f"1. Go to Settings (βš™οΈ) in your HuggingFace Space\
273
+ "
274
+ error_msg += f"2. Click 'Variables and secrets'\
275
+ "
276
+ error_msg += f"3. Add secret: **{API_KEY_VAR}**\
277
+ "
278
+ error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\n\
279
+ "
280
  error_msg += f"Get your API key at: https://openrouter.ai/keys"
281
+ print(f"❌ API request failed: No API key configured for {API_KEY_VAR}")
282
  return error_msg
283
 
284
  # Get grounding context
 
295
  content = fetch_url_content(url)
296
  dynamic_context_parts.append(f"\n\nDynamic context from {url}:\n{content}")
297
  if dynamic_context_parts:
298
+ grounding_context += "\
299
+ ".join(dynamic_context_parts)
300
 
301
  # Build enhanced system prompt with grounding context
302
  enhanced_system_prompt = SYSTEM_PROMPT + grounding_context
 
374
  print(f"❌ Failed to parse API response: {str(e)}")
375
  return f"API Error: Failed to parse response - {str(e)}"
376
  elif response.status_code == 401:
377
+ error_msg = f"πŸ” **Authentication Error**\n\
378
+ "
379
+ error_msg += f"Your API key appears to be invalid or expired.\n\
380
+ "
381
+ error_msg += f"**Troubleshooting:**\
382
+ "
383
+ error_msg += f"1. Check that your **{API_KEY_VAR}** secret is set correctly\
384
+ "
385
+ error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\
386
+ "
387
+ error_msg += f"3. Ensure your key starts with `sk-or-`\
388
+ "
389
  error_msg += f"4. Check that you have credits on your OpenRouter account"
390
  print(f"❌ API authentication failed: {response.status_code} - {response.text[:200]}")
391
  return error_msg
392
  elif response.status_code == 429:
393
+ error_msg = f"⏱️ **Rate Limit Exceeded**\n\
394
+ "
395
+ error_msg += f"Too many requests. Please wait a moment and try again.\n\
396
+ "
397
+ error_msg += f"**Troubleshooting:**\
398
+ "
399
  error_msg += f"1. Wait 30-60 seconds before trying again\n"
400
  error_msg += f"2. Check your OpenRouter usage limits\n"
401
  error_msg += f"3. Consider upgrading your OpenRouter plan"
 
408
  except:
409
  error_message = response.text
410
 
411
+ error_msg = f"⚠️ **Request Error**\n\
412
+ "
413
  error_msg += f"The API request was invalid:\n"
414
+ error_msg += f"`{error_message}`\n\
415
+ "
416
  if "model" in error_message.lower():
417
  error_msg += f"**Model Issue:** The model `{MODEL}` may not be available.\n"
418
  error_msg += f"Try switching to a different model in your Space configuration."
419
  print(f"❌ Bad request: {response.status_code} - {error_message}")
420
  return error_msg
421
  else:
422
+ error_msg = f"🚫 **API Error {response.status_code}**\n\
423
+ "
424
+ error_msg += f"An unexpected error occurred. Please try again.\n\
425
+ "
426
  error_msg += f"If this persists, check:\n"
427
  error_msg += f"1. OpenRouter service status\n"
428
  error_msg += f"2. Your API key and credits\n"
 
431
  return error_msg
432
 
433
  except requests.exceptions.Timeout:
434
+ error_msg = f"⏰ **Request Timeout**\n\
435
+ "
436
+ error_msg += f"The API request took too long (30s limit).\n\
437
+ "
438
  error_msg += f"**Troubleshooting:**\n"
439
  error_msg += f"1. Try again with a shorter message\n"
440
  error_msg += f"2. Check your internet connection\n"
 
442
  print(f"❌ Request timeout after 30 seconds")
443
  return error_msg
444
  except requests.exceptions.ConnectionError:
445
+ error_msg = f"🌐 **Connection Error**\n\
446
+ "
447
+ error_msg += f"Could not connect to OpenRouter API.\n\
448
+ "
449
  error_msg += f"**Troubleshooting:**\n"
450
  error_msg += f"1. Check your internet connection\n"
451
  error_msg += f"2. Check OpenRouter service status\n"
 
453
  print(f"❌ Connection error to OpenRouter API")
454
  return error_msg
455
  except Exception as e:
456
+ error_msg = "❌ **Unexpected Error**\n\
457
+ "
458
+ error_msg += "An unexpected error occurred:\n"
459
+ error_msg += f"`{str(e)}`\n\
460
+ "
461
+ error_msg += "Please try again or contact support if this persists."
462
  print(f"❌ Unexpected error: {str(e)}")
463
  return error_msg
464
 
 
542
 
543
  # Configuration status display
544
  def get_configuration_status():
545
+ """Generate a clean configuration status message for display"""
546
  status_parts = []
547
 
548
+ # Basic configuration info (without redundant "Configuration:" header)
549
+ status_parts.append(f"**Name:** {SPACE_NAME}")
550
+ status_parts.append(f"**Model:** {MODEL}")
551
+ status_parts.append(f"**Theme:** {THEME}")
 
 
 
 
 
 
 
 
552
  status_parts.append(f"**Temperature:** 0.7")
553
+ status_parts.append(f"**Max Response Tokens:** 750")
554
+ status_parts.append("")
555
+
556
+ # Example prompts
557
+ status_parts.append("")
558
+ examples_list = config.get('examples', [])
559
+ if isinstance(examples_list, str):
560
+ try:
561
+ import ast
562
+ examples_list = ast.literal_eval(examples_list)
563
+ except:
564
+ examples_list = []
565
+
566
+ if examples_list and len(examples_list) > 0:
567
+ status_parts.append("**Example Prompts:**")
568
+ for example in examples_list[:5]: # Show up to 5 examples
569
+ status_parts.append(f"β€’ {example}")
570
+ if len(examples_list) > 5:
571
+ status_parts.append(f"β€’ ... and {len(examples_list) - 5} more")
572
+ else:
573
+ status_parts.append("**Example Prompts:** No example prompts configured")
574
 
575
  # URL Context if configured
576
+ if GROUNDING_URLS and len(GROUNDING_URLS) > 0:
577
+ status_parts.append("")
578
+ status_parts.append("**Grounding URLs:**")
579
+ for i, url in enumerate(GROUNDING_URLS[:5], 1): # Show first 5 URLs
580
+ status_parts.append(f"{i}. {url}")
581
+ if len(GROUNDING_URLS) > 5:
582
+ status_parts.append(f"... and {len(GROUNDING_URLS) - 5} more URLs")
583
+
584
+ # System prompt at the end
585
+ status_parts.append("")
586
+ status_parts.append(f"**System Prompt:** {SYSTEM_PROMPT}")
587
+
588
+ # API Key status (minimal, at the end)
589
+ status_parts.append("")
590
+ if not API_KEY_VALID:
591
+ status_parts.append(f"**Note:** API key ({API_KEY_VAR}) not configured in Space secrets")
 
 
 
 
 
592
 
593
  return "\n".join(status_parts)
594
 
 
614
 
615
  # Main chat interface (hidden until access granted)
616
  with gr.Column(visible=(ACCESS_CODE is None)) as chat_section:
617
+ # Get examples from config
618
+ examples = config.get('examples', [])
619
+ if isinstance(examples, str):
620
+ try:
621
+ import ast
622
+ examples = ast.literal_eval(examples)
623
+ except:
624
+ examples = []
625
+
626
  chat_interface = gr.ChatInterface(
627
  fn=store_and_generate_response, # Use wrapper function to store history
628
  title="", # Title already shown above
629
  description="", # Description already shown above
630
+ examples=examples if examples else None,
631
  type="messages" # Use modern message format for better compatibility
632
  )
633
 
 
642
  outputs=[export_file]
643
  )
644
 
645
+ # Configuration status
646
+ with gr.Accordion("Configuration", open=False):
647
+ gr.Markdown(get_configuration_status())
648
 
649
  # Connect access verification
650
  if ACCESS_CODE is not None:
 
691
  with open('config.json', 'r') as f:
692
  current_config = json.load(f)
693
  except:
694
+ # Use DEFAULT_CONFIG as fallback
695
+ current_config = DEFAULT_CONFIG.copy()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
 
697
+ # Editable fields
698
+ # System Prompt
699
  edit_system_prompt = gr.Textbox(
700
  label="System Prompt",
701
  value=current_config.get('system_prompt', SYSTEM_PROMPT),
 
717
  ],
718
  value=current_config.get('model', MODEL)
719
  )
720
+
721
+ # 4. Example prompts field
722
  examples_value = current_config.get('examples', [])
723
  if isinstance(examples_value, list):
724
  examples_text_value = "\n".join(examples_value)
 
749
  step=50
750
  )
751
 
752
+ # URL Grounding fields
753
  gr.Markdown("### URL Grounding")
754
  grounding_urls_value = current_config.get('grounding_urls', [])
755
  if isinstance(grounding_urls_value, str):
 
776
  )
777
 
778
  with gr.Row():
779
+ save_config_btn = gr.Button("Save Configuration", variant="primary")
780
+ reset_config_btn = gr.Button("Reset to Defaults", variant="secondary")
781
 
782
  config_status = gr.Markdown("")
783
 
 
785
  def verify_faculty_password(password):
786
  if password == FACULTY_PASSWORD:
787
  return (
788
+ gr.update(value="Authentication successful!"),
789
  gr.update(visible=False), # Hide auth row
790
  gr.update(visible=True), # Show config section
791
  True # Update auth state
792
  )
793
  else:
794
  return (
795
+ gr.update(value="Invalid password"),
796
  gr.update(visible=True), # Keep auth row visible
797
  gr.update(visible=False), # Keep config hidden
798
  False # Auth failed
799
  )
800
 
801
  # Save configuration function
802
+ def save_configuration(new_prompt, new_model, new_examples, new_temp, new_tokens, *url_values, lock_config, is_authenticated):
 
 
 
 
 
 
 
 
 
803
  if not is_authenticated:
804
+ return "Not authenticated"
805
 
806
  # Check if configuration is already locked
807
  try:
808
  with open('config.json', 'r') as f:
809
  existing_config = json.load(f)
810
  if existing_config.get('locked', False):
811
+ return "Configuration is locked and cannot be modified"
812
  except:
813
  pass
814
 
 
817
  with open('config.json', 'r') as f:
818
  current_full_config = json.load(f)
819
  except:
820
+ # If config.json doesn't exist, use default configuration
821
+ current_full_config = DEFAULT_CONFIG.copy()
822
 
823
  # Process example prompts
824
  examples_list = [ex.strip() for ex in new_examples.split('\n') if ex.strip()]
825
 
826
+ # Process URL values - lock_config is the last parameter
827
+ urls = list(url_values[:-1]) # All but last are URLs
828
+ lock_config_from_args = url_values[-1] # Last is lock_config
829
+ # Filter out empty URLs
830
+ grounding_urls = [url.strip() for url in urls if url.strip()]
831
 
832
  # Update all editable fields while preserving everything else
833
  current_full_config.update({
 
 
834
  'system_prompt': new_prompt,
835
  'model': new_model,
836
  'examples': examples_list,
837
  'temperature': new_temp,
838
  'max_tokens': int(new_tokens),
839
  'grounding_urls': grounding_urls,
840
+ 'locked': lock_config_from_args,
841
  'last_modified': datetime.now().isoformat(),
842
  'last_modified_by': 'faculty'
843
  })
 
846
  with open('config.json', 'w') as f:
847
  json.dump(current_full_config, f, indent=2)
848
 
849
+ # Optional: Auto-commit to HuggingFace if token is available
850
+ hf_token = os.environ.get("HF_TOKEN")
851
+ space_id = os.environ.get("SPACE_ID")
 
 
 
 
 
 
852
 
853
+ if hf_token and space_id:
854
+ try:
855
+ from huggingface_hub import HfApi
856
+ api = HfApi()
857
+ api.upload_file(
858
+ path_or_fileobj="config.json",
859
+ path_in_repo="config.json",
860
+ repo_id=space_id,
861
+ repo_type="space",
862
+ commit_message=f"Update configuration by faculty at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
863
+ )
864
+ return f"βœ… Configuration saved and committed to repository at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\nπŸ”„ **Space will restart automatically** to apply changes."
865
+ except Exception as commit_error:
866
+ print(f"Note: Could not auto-commit to repository: {commit_error}")
867
+ return f"βœ… Configuration saved locally at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\nπŸ”„ **Manual Restart Required**\nFor changes to take effect:\n1. Go to Settings (βš™οΈ)\n2. Click 'Factory reboot'\n3. Wait ~30 seconds for restart"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
868
  else:
869
+ return f"βœ… Configuration saved at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\nπŸ”„ **Manual Restart Required**\nFor changes to take effect:\n1. Go to Settings (βš™οΈ)\n2. Click 'Factory reboot'\n3. Wait ~30 seconds for restart"
 
 
870
  except Exception as e:
871
  return f"❌ Error saving configuration: {str(e)}"
872
 
873
  # Reset configuration function
874
  def reset_configuration(is_authenticated):
875
  if not is_authenticated:
876
+ updates = ["Not authenticated"] + [gr.update() for _ in range(14)] # 1 status + 14 fields (prompt, model, examples, temp, tokens + 10 urls)
877
  return tuple(updates)
878
 
879
  # Check if locked
 
881
  with open('config.json', 'r') as f:
882
  existing_config = json.load(f)
883
  if existing_config.get('locked', False):
884
+ updates = ["Configuration is locked"] + [gr.update() for _ in range(14)]
885
  return tuple(updates)
886
  except:
887
  pass
 
893
  else:
894
  examples_text = ""
895
 
896
+ # Get default URLs - parse from JSON string if needed
897
  default_urls = DEFAULT_CONFIG.get('grounding_urls', [])
898
  if isinstance(default_urls, str):
899
  try:
900
+ import json
901
+ default_urls = json.loads(default_urls)
902
  except:
903
  default_urls = []
904
+ elif not isinstance(default_urls, list):
905
+ default_urls = []
906
 
907
  # Reset to original default values
908
  updates = [
909
+ "Reset to default values",
 
 
910
  gr.update(value=DEFAULT_CONFIG.get('system_prompt', SYSTEM_PROMPT)),
911
  gr.update(value=DEFAULT_CONFIG.get('model', MODEL)),
912
  gr.update(value=examples_text),
 
937
  # Connect configuration buttons
938
  save_config_btn.click(
939
  save_configuration,
940
+ inputs=[edit_system_prompt, edit_model, edit_examples, edit_temperature, edit_max_tokens] + url_fields + [config_locked, faculty_auth_state],
941
  outputs=[config_status]
942
  )
943
 
944
  reset_config_btn.click(
945
  reset_configuration,
946
  inputs=[faculty_auth_state],
947
+ outputs=[config_status, edit_system_prompt, edit_model, edit_examples, edit_temperature, edit_max_tokens] + url_fields
948
  )
949
  else:
950
+ gr.Markdown("Faculty configuration is not enabled. Set CONFIG_CODE in Space secrets to enable.")
951
 
952
  if __name__ == "__main__":
953
  demo.launch()
config.json CHANGED
@@ -1,13 +1,15 @@
1
  {
2
  "name": "AI Assistant",
3
  "description": "A customizable AI assistant",
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": "google/gemini-2.0-flash-001",
6
  "api_key_var": "API_KEY",
7
  "temperature": 0.7,
8
  "max_tokens": 750,
9
- "examples": "['Hello! How can you help me?', 'Tell me something interesting', 'What can you do?']",
10
- "grounding_urls": "[]",
 
 
11
  "enable_dynamic_urls": true,
12
  "theme": "Default"
13
  }
 
1
  {
2
  "name": "AI Assistant",
3
  "description": "A customizable AI assistant",
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": "API_KEY",
7
  "temperature": 0.7,
8
  "max_tokens": 750,
9
+ "examples": [
10
+ "Can you help me understand why the sky is blue?"
11
+ ],
12
+ "grounding_urls": [],
13
  "enable_dynamic_urls": true,
14
  "theme": "Default"
15
  }