milwright commited on
Commit
15b5dc3
·
verified ·
1 Parent(s): f96aada

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +340 -232
  2. config.json +11 -6
app.py CHANGED
@@ -13,101 +13,69 @@ import urllib.parse
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': "AI Assistant",
19
- 'description': "A customizable AI assistant",
20
- 'system_prompt': """You are an expert literary analysis instructor specializing in close reading techniques across genres, periods, and theoretical approaches. Your role is to help students develop sophisticated reading practices connecting textual details to interpretive arguments. Follow the close reading framework below:
21
-
22
- 1. Reading Process
23
- 1a. Initial Analysis: ensure literal comprehension; identify literary devices and effects; examine diction, syntax, imagery
24
- 1b. Pattern Recognition: trace recurring motifs, tensions, contradictions; analyze formal elements by genre; connect style to meaning
25
- 1c. Interpretive Synthesis: apply theoretical lenses when appropriate; build arguments through textual evidence; acknowledge interpretive complexity
26
-
27
- 2. Genre-Specific Techniques:
28
- 2a. Poetry: prosody, enjambment, sound patterns, visual arrangement
29
- 2b. Fiction: narration, characterization, setting, temporal structure
30
- 2c. Drama: dialogue patterns, stage directions, performance considerations
31
-
32
- 3. Critical Approaches:
33
- 3a. **Formal Analysis**: unity, ambiguity, paradox, irony
34
- 3b. **Contextual Reading**: historical, biographical, cultural frameworks
35
- 3c. **Theoretical Applications**: psychoanalytic, Marxist, feminist, postcolonial""",
36
- 'temperature': 0.7,
37
- 'max_tokens': 1500,
38
- 'model': "nvidia/llama-3.1-nemotron-70b-instruct",
39
- 'api_key_var': "API_KEY",
40
- 'theme': "Origin",
41
- 'grounding_urls': ["https://owl.purdue.edu/owl/subject_specific_writing/writing_in_literature/writing_about_fiction/index.html", "https://writingcenter.fas.harvard.edu/pages/how-do-close-reading"],
42
- 'enable_dynamic_urls': True,
43
- 'examples': ['How do I analyze symbolism in this passage?', 'Help me apply feminist theory to this text', 'What should I look for in poetry analysis?', 'How do I write a literary analysis paper?'],
44
- 'locked': False
45
- }
46
-
47
- # Load configuration from file - this is the single source of truth
48
- def load_config():
49
- """Load configuration from config.json with fallback to defaults"""
50
- try:
51
- with open('config.json', 'r') as f:
52
- config = json.load(f)
53
- print("✅ Loaded configuration from config.json")
54
- return config
55
- except FileNotFoundError:
56
- print("ℹ️ No config.json found, using default configuration")
57
- # Save default config for future use
58
- try:
59
- with open('config.json', 'w') as f:
60
- json.dump(DEFAULT_CONFIG, f, indent=2)
61
- print("✅ Created config.json with default values")
62
- except:
63
- pass
64
- return DEFAULT_CONFIG
65
- except Exception as e:
66
- print(f"⚠️ Error loading config.json: {e}, using defaults")
67
- return DEFAULT_CONFIG
68
-
69
- # Load configuration
70
- config = load_config()
71
-
72
- # Function to reload configuration values from config
73
- def reload_config_values():
74
- """Reload all configuration values from the config dict"""
75
- global SPACE_NAME, SPACE_DESCRIPTION, SYSTEM_PROMPT, temperature, max_tokens, MODEL, THEME, GROUNDING_URLS, ENABLE_DYNAMIC_URLS, API_KEY_VAR
76
-
77
- # Reload config from file
78
- global config
79
- config = load_config()
80
-
81
- # Update all global variables
82
- SPACE_NAME = config.get('name', DEFAULT_CONFIG['name'])
83
- SPACE_DESCRIPTION = config.get('description', DEFAULT_CONFIG['description'])
84
- SYSTEM_PROMPT = config.get('system_prompt', DEFAULT_CONFIG['system_prompt'])
85
- temperature = config.get('temperature', DEFAULT_CONFIG['temperature'])
86
- max_tokens = config.get('max_tokens', DEFAULT_CONFIG['max_tokens'])
87
- MODEL = config.get('model', DEFAULT_CONFIG['model'])
88
- THEME = config.get('theme', DEFAULT_CONFIG['theme'])
89
- GROUNDING_URLS = config.get('grounding_urls', DEFAULT_CONFIG['grounding_urls'])
90
- ENABLE_DYNAMIC_URLS = config.get('enable_dynamic_urls', DEFAULT_CONFIG['enable_dynamic_urls'])
91
- API_KEY_VAR = config.get('api_key_var', DEFAULT_CONFIG['api_key_var'])
92
-
93
- # Initial load of configuration values
94
- SPACE_NAME = config.get('name', DEFAULT_CONFIG['name'])
95
- SPACE_DESCRIPTION = config.get('description', DEFAULT_CONFIG['description'])
96
- SYSTEM_PROMPT = config.get('system_prompt', DEFAULT_CONFIG['system_prompt'])
97
- temperature = config.get('temperature', DEFAULT_CONFIG['temperature'])
98
- max_tokens = config.get('max_tokens', DEFAULT_CONFIG['max_tokens'])
99
- MODEL = config.get('model', DEFAULT_CONFIG['model'])
100
- THEME = config.get('theme', DEFAULT_CONFIG['theme'])
101
- GROUNDING_URLS = config.get('grounding_urls', DEFAULT_CONFIG['grounding_urls'])
102
- ENABLE_DYNAMIC_URLS = config.get('enable_dynamic_urls', DEFAULT_CONFIG['enable_dynamic_urls'])
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  # Get access code from environment variable for security
105
  # If ACCESS_CODE is not set, no access control is applied
106
  ACCESS_CODE = os.environ.get("ACCESS_CODE")
 
107
 
108
  # Get API key from environment - customizable variable name with validation
109
- API_KEY_VAR = config.get('api_key_var', DEFAULT_CONFIG['api_key_var'])
110
- API_KEY = os.environ.get(API_KEY_VAR)
111
  if API_KEY:
112
  API_KEY = API_KEY.strip() # Remove any whitespace
113
  if not API_KEY: # Check if empty after stripping
@@ -118,21 +86,21 @@ def validate_api_key():
118
  """Validate API key configuration with detailed logging"""
119
  if not API_KEY:
120
  print(f"⚠️ API KEY CONFIGURATION ERROR:")
121
- print(f" Variable name: {API_KEY_VAR}")
122
  print(f" Status: Not set or empty")
123
- print(f" Action needed: Set '{API_KEY_VAR}' in HuggingFace Space secrets")
124
  print(f" Expected format: sk-or-xxxxxxxxxx")
125
  return False
126
  elif not API_KEY.startswith('sk-or-'):
127
  print(f"⚠️ API KEY FORMAT WARNING:")
128
- print(f" Variable name: {API_KEY_VAR}")
129
  print(f" Current value: {API_KEY[:10]}..." if len(API_KEY) > 10 else API_KEY)
130
  print(f" Expected format: sk-or-xxxxxxxxxx")
131
  print(f" Note: OpenRouter keys should start with 'sk-or-'")
132
  return True # Still try to use it
133
  else:
134
  print(f"✅ API Key configured successfully")
135
- print(f" Variable: {API_KEY_VAR}")
136
  print(f" Format: Valid OpenRouter key")
137
  return True
138
 
@@ -298,10 +266,10 @@ def generate_response(message, history):
298
  error_msg += f"Please configure your OpenRouter API key:\n"
299
  error_msg += f"1. Go to Settings (⚙️) in your HuggingFace Space\n"
300
  error_msg += f"2. Click 'Variables and secrets'\n"
301
- error_msg += f"3. Add secret: **{API_KEY_VAR}**\n"
302
  error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\n\n"
303
  error_msg += f"Get your API key at: https://openrouter.ai/keys"
304
- print(f"❌ API request failed: No API key configured for {API_KEY_VAR}")
305
  return error_msg
306
 
307
  # Get grounding context
@@ -360,7 +328,7 @@ def generate_response(message, history):
360
  "model": MODEL,
361
  "messages": messages,
362
  "temperature": 0.7,
363
- "max_tokens": 1500
364
  },
365
  timeout=30
366
  )
@@ -399,7 +367,7 @@ def generate_response(message, history):
399
  error_msg = f"🔐 **Authentication Error**\n\n"
400
  error_msg += f"Your API key appears to be invalid or expired.\n\n"
401
  error_msg += f"**Troubleshooting:**\n"
402
- error_msg += f"1. Check that your **{API_KEY_VAR}** secret is set correctly\n"
403
  error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\n"
404
  error_msg += f"3. Ensure your key starts with `sk-or-`\n"
405
  error_msg += f"4. Check that you have credits on your OpenRouter account"
@@ -474,14 +442,14 @@ def verify_access_code(code):
474
  global _access_granted_global
475
  if ACCESS_CODE is None:
476
  _access_granted_global = True
477
- return gr.update(visible=False), gr.update(visible=True), gr.update(value=True)
478
 
479
  if code == ACCESS_CODE:
480
  _access_granted_global = True
481
- return gr.update(visible=False), gr.update(visible=True), gr.update(value=True)
482
  else:
483
  _access_granted_global = False
484
- return gr.update(visible=True, value="❌ Incorrect access code. Please try again."), gr.update(visible=False), gr.update(value=False)
485
 
486
  def protected_generate_response(message, history):
487
  """Protected response function that checks access"""
@@ -545,47 +513,46 @@ def export_conversation(history):
545
 
546
  # Configuration status display
547
  def get_configuration_status():
548
- """Generate a clean configuration status message for display"""
549
  status_parts = []
550
 
551
- status_parts.append("**Configuration:**")
552
- status_parts.append("")
553
- status_parts.append(f"**Name:** {SPACE_NAME}")
554
- status_parts.append(f"**Model:** {MODEL}")
555
- status_parts.append(f"**Theme:** {THEME}")
556
- status_parts.append(f"**Temperature:** {temperature}")
557
- status_parts.append(f"**Max Response Tokens:** {max_tokens}")
558
- status_parts.append("")
559
- status_parts.append(f"**System Prompt:** {SYSTEM_PROMPT}")
560
-
561
- # Example prompts
562
- status_parts.append("")
563
- examples_list = config.get('examples', [])
564
- if isinstance(examples_list, str):
565
- try:
566
- import ast
567
- examples_list = ast.literal_eval(examples_list)
568
- except:
569
- examples_list = []
570
-
571
- if examples_list and len(examples_list) > 0:
572
- status_parts.append("**Example Prompts:**")
573
- for example in examples_list[:5]: # Show up to 5 examples
574
- status_parts.append(f"• {example}")
575
- if len(examples_list) > 5:
576
- status_parts.append(f"• ... and {len(examples_list) - 5} more")
577
  else:
578
- status_parts.append("**Example Prompts:** No example prompts configured")
 
 
 
 
 
 
 
 
579
 
580
  # URL Context if configured
581
- if GROUNDING_URLS and len(GROUNDING_URLS) > 0:
582
- status_parts.append("")
583
- status_parts.append(f"**Grounding URLs:** {len(GROUNDING_URLS)} configured")
 
 
 
 
 
 
 
 
 
 
 
584
 
585
- # API Key status (minimal, at the end)
586
- status_parts.append("")
587
- if not API_KEY_VALID:
588
- status_parts.append(f"**Note:** API key ({API_KEY_VAR}) not configured in Space secrets")
 
 
589
 
590
  return "\n".join(status_parts)
591
 
@@ -615,7 +582,7 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
615
  fn=store_and_generate_response, # Use wrapper function to store history
616
  title="", # Title already shown above
617
  description="", # Description already shown above
618
- examples=['How do I analyze symbolism in this passage?', 'Help me apply feminist theory to this text', 'What should I look for in poetry analysis?', 'How do I write a literary analysis paper?'],
619
  type="messages" # Use modern message format for better compatibility
620
  )
621
 
@@ -630,26 +597,23 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
630
  outputs=[export_file]
631
  )
632
 
633
- # Configuration status
634
- with gr.Accordion("Configuration", open=False):
635
- gr.Markdown(get_configuration_status())
636
 
637
  # Connect access verification
638
  if ACCESS_CODE is not None:
639
  access_btn.click(
640
  verify_access_code,
641
  inputs=[access_input],
642
- outputs=[access_error, chat_section, access_granted]
643
  )
644
  access_input.submit(
645
  verify_access_code,
646
  inputs=[access_input],
647
- outputs=[access_error, chat_section, access_granted]
648
  )
649
 
650
  # Faculty Configuration Section - appears at the bottom with password protection
651
- with gr.Accordion("Faculty Configuration", open=False, visible=True) as faculty_section:
652
- gr.Markdown("Edit assistant configuration. Requires CONFIG_CODE secret.")
653
 
654
  # Check if faculty password is configured
655
  FACULTY_PASSWORD = os.environ.get("CONFIG_CODE", "").strip()
@@ -680,25 +644,64 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
680
  current_config = json.load(f)
681
  except:
682
  current_config = {
 
 
 
 
683
  'system_prompt': SYSTEM_PROMPT,
 
 
684
  'temperature': 0.7,
685
- 'max_tokens': 1500,
686
  'locked': False
687
  }
688
 
689
  # Editable fields
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
690
  edit_system_prompt = gr.Textbox(
691
  label="System Prompt",
692
  value=current_config.get('system_prompt', SYSTEM_PROMPT),
693
  lines=5
694
  )
695
 
696
- # Example prompts field
697
- examples_value = current_config.get('examples', [])
698
- if isinstance(examples_value, list):
699
- examples_text_value = "\n".join(examples_value)
700
- else:
701
- examples_text_value = ""
702
 
703
  edit_examples = gr.Textbox(
704
  label="Example Prompts (one per line)",
@@ -719,30 +722,43 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
719
  label="Max Tokens",
720
  minimum=50,
721
  maximum=4096,
722
- value=current_config.get('max_tokens', 1500),
723
  step=50
724
  )
725
 
726
  # URL Grounding fields
727
- gr.Markdown("### URL Grounding")
728
- grounding_urls_value = current_config.get('grounding_urls', [])
729
- if isinstance(grounding_urls_value, str):
 
 
 
730
  try:
731
  import ast
732
- grounding_urls_value = ast.literal_eval(grounding_urls_value)
733
  except:
734
- grounding_urls_value = []
735
 
736
- # Create 10 URL input fields
737
- url_fields = []
738
- for i in range(10):
739
- url_value = grounding_urls_value[i] if i < len(grounding_urls_value) else ""
740
- url_field = gr.Textbox(
741
- label=f"URL {i+1}" + (" (Primary)" if i < 2 else " (Secondary)"),
742
- value=url_value,
743
- placeholder="https://..."
744
- )
745
- url_fields.append(url_field)
 
 
 
 
 
 
 
 
 
 
746
 
747
  config_locked = gr.Checkbox(
748
  label="Lock Configuration (Prevent further edits)",
@@ -750,8 +766,8 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
750
  )
751
 
752
  with gr.Row():
753
- save_config_btn = gr.Button("Save Configuration", variant="primary")
754
- reset_config_btn = gr.Button("Reset to Defaults", variant="secondary")
755
 
756
  config_status = gr.Markdown("")
757
 
@@ -759,77 +775,174 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
759
  def verify_faculty_password(password):
760
  if password == FACULTY_PASSWORD:
761
  return (
762
- gr.update(value="Authentication successful!"),
763
  gr.update(visible=False), # Hide auth row
764
  gr.update(visible=True), # Show config section
765
  True # Update auth state
766
  )
767
  else:
768
  return (
769
- gr.update(value="Invalid password"),
770
  gr.update(visible=True), # Keep auth row visible
771
  gr.update(visible=False), # Keep config hidden
772
  False # Auth failed
773
  )
774
 
775
  # Save configuration function
776
- def save_configuration(new_prompt, new_examples, new_temp, new_tokens, *url_values, lock_config, is_authenticated):
777
  if not is_authenticated:
778
- return "Not authenticated"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
779
 
780
  # Check if configuration is already locked
781
  try:
782
  with open('config.json', 'r') as f:
783
  existing_config = json.load(f)
784
  if existing_config.get('locked', False):
785
- return "Configuration is locked and cannot be modified"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
786
  except:
787
  pass
788
 
789
- # Load current config to preserve all values
790
- try:
791
- with open('config.json', 'r') as f:
792
- current_full_config = json.load(f)
793
- except:
794
- # If config.json doesn't exist, use global config
795
- current_full_config = config.copy()
796
-
797
- # Process example prompts
798
  examples_list = [ex.strip() for ex in new_examples.split('\n') if ex.strip()]
799
 
800
- # Process URL values - extract the 10 URL values and lock_config from varargs
801
- urls = list(url_values[:10]) # First 10 are URLs
802
- lock_config = url_values[10] # 11th is lock_config
803
- # Filter out empty URLs
804
  grounding_urls = [url.strip() for url in urls if url.strip()]
805
 
806
- # Update only the editable fields while preserving everything else
807
- current_full_config.update({
 
 
 
 
808
  'system_prompt': new_prompt,
809
  'examples': examples_list,
 
810
  'temperature': new_temp,
811
  'max_tokens': int(new_tokens),
812
- 'grounding_urls': grounding_urls,
813
  'locked': lock_config,
814
  'last_modified': datetime.now().isoformat(),
815
  'modified_by': 'faculty'
816
- })
817
 
818
  try:
819
  with open('config.json', 'w') as f:
820
- json.dump(current_full_config, f, indent=2)
 
821
 
822
- # Reload all configuration values from the updated config.json
823
- reload_config_values()
 
 
 
 
 
 
 
 
 
824
 
825
- return f"Configuration saved successfully at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826
  except Exception as e:
827
- return f"Error saving configuration: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
828
 
829
  # Reset configuration function
830
  def reset_configuration(is_authenticated):
831
  if not is_authenticated:
832
- updates = ["Not authenticated"] + [gr.update() for _ in range(14)] # 1 status + 14 fields
833
  return tuple(updates)
834
 
835
  # Check if locked
@@ -837,42 +950,33 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
837
  with open('config.json', 'r') as f:
838
  existing_config = json.load(f)
839
  if existing_config.get('locked', False):
840
- updates = ["Configuration is locked"] + [gr.update() for _ in range(14)]
841
  return tuple(updates)
842
  except:
843
  pass
844
 
845
- # Get default examples as text
846
- default_examples = DEFAULT_CONFIG.get('examples', [])
847
- if isinstance(default_examples, list):
848
- examples_text = "\n".join(default_examples)
849
- else:
850
- examples_text = ""
851
-
852
- # Get default URLs
853
- default_urls = DEFAULT_CONFIG.get('grounding_urls', [])
854
- if isinstance(default_urls, str):
855
- try:
856
- import ast
857
- default_urls = ast.literal_eval(default_urls)
858
- except:
859
- default_urls = []
860
-
861
- # Reset to original default values
862
- updates = [
863
- "Reset to default values",
864
- gr.update(value=DEFAULT_CONFIG.get('system_prompt', '')),
865
- gr.update(value=examples_text),
866
- gr.update(value=DEFAULT_CONFIG.get('temperature', 0.7)),
867
- gr.update(value=DEFAULT_CONFIG.get('max_tokens', 500))
868
- ]
869
-
870
- # Add URL updates
871
- for i in range(10):
872
- url_value = default_urls[i] if i < len(default_urls) else ""
873
- updates.append(gr.update(value=url_value))
874
-
875
- return tuple(updates)
876
 
877
  # Connect authentication
878
  faculty_auth_btn.click(
@@ -890,17 +994,21 @@ with gr.Blocks(title=SPACE_NAME, theme=theme_class()) as demo:
890
  # Connect configuration buttons
891
  save_config_btn.click(
892
  save_configuration,
893
- inputs=[edit_system_prompt, edit_examples, edit_temperature, edit_max_tokens] + url_fields + [config_locked, faculty_auth_state],
894
- outputs=[config_status]
 
 
 
895
  )
896
 
897
  reset_config_btn.click(
898
  reset_configuration,
899
  inputs=[faculty_auth_state],
900
- outputs=[config_status, edit_system_prompt, edit_examples, edit_temperature, edit_max_tokens] + url_fields
 
901
  )
902
  else:
903
- gr.Markdown("Faculty configuration is not enabled. Set CONFIG_CODE in Space secrets to enable.")
904
 
905
  if __name__ == "__main__":
906
  demo.launch()
 
13
  SPACE_NAME = "AI Assistant"
14
  SPACE_DESCRIPTION = "A customizable AI assistant"
15
 
16
+ # Default configuration values
17
+ DEFAULT_SYSTEM_PROMPT = """You are an AI assistant specialized in mathematics and statistics who guides users through problem-solving rather than providing direct answers. You help users discover solutions by asking strategic questions ('What do we know so far?' 'What method might apply here?' 'Can you identify a pattern?'), prompting them to explain their reasoning, and offering hints that build on their current understanding. Format all mathematical expressions in LaTeX (inline: $x^2 + y^2 = r^2$, display: $$\int_a^b f(x)dx$$). When users are stuck, provide scaffolded support: suggest examining simpler cases, identifying relevant formulas or theorems, or breaking the problem into smaller parts. Use multiple representations to illuminate different aspects of the problem, validate partial progress to build confidence, and help users recognize and correct their own errors through targeted questions rather than corrections. Your goal is to develop problem-solving skills and mathematical reasoning, not just arrive at answers."""
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
+ SPACE_NAME = saved_config.get('name', "AI Assistant")
26
+ SPACE_DESCRIPTION = saved_config.get('description', "A customizable AI assistant")
27
+ MODEL = saved_config.get('model', "google/gemini-2.0-flash-001")
28
+ THEME = saved_config.get('theme', "Default")
29
+ SYSTEM_PROMPT = saved_config.get('system_prompt', DEFAULT_SYSTEM_PROMPT)
30
+ temperature = saved_config.get('temperature', DEFAULT_TEMPERATURE)
31
+ max_tokens = saved_config.get('max_tokens', DEFAULT_MAX_TOKENS)
32
+
33
+ # Load grounding URLs
34
+ saved_urls = saved_config.get('grounding_urls', [])
35
+ if isinstance(saved_urls, str):
36
+ try:
37
+ import ast
38
+ GROUNDING_URLS = ast.literal_eval(saved_urls)
39
+ except:
40
+ GROUNDING_URLS = []
41
+ else:
42
+ GROUNDING_URLS = saved_urls
43
+
44
+ # Load examples
45
+ saved_examples = saved_config.get('examples', ['Hello! How can you help me?', 'Tell me something interesting', 'What can you do?'])
46
+ if isinstance(saved_examples, str):
47
+ try:
48
+ import ast
49
+ EXAMPLES = ast.literal_eval(saved_examples)
50
+ except:
51
+ EXAMPLES = ['Hello! How can you help me?', 'Tell me something interesting', 'What can you do?']
52
+ elif isinstance(saved_examples, list):
53
+ EXAMPLES = saved_examples
54
+ else:
55
+ EXAMPLES = ['Hello! How can you help me?', 'Tell me something interesting', 'What can you do?']
56
+
57
+ print("✅ Loaded configuration from config.json")
58
+ except:
59
+ # Use defaults if no config file or error
60
+ SPACE_NAME = "AI Assistant"
61
+ SPACE_DESCRIPTION = "A customizable AI assistant"
62
+ MODEL = "google/gemini-2.0-flash-001"
63
+ THEME = "Default"
64
+ SYSTEM_PROMPT = DEFAULT_SYSTEM_PROMPT
65
+ temperature = DEFAULT_TEMPERATURE
66
+ max_tokens = DEFAULT_MAX_TOKENS
67
+ GROUNDING_URLS = []
68
+ EXAMPLES = ['Hello! How can you help me?', 'Tell me something interesting', 'What can you do?']
69
+ print("ℹ️ Using default configuration")
70
+
71
+ # MODEL, THEME, and GROUNDING_URLS are now set by config loading above
72
  # Get access code from environment variable for security
73
  # If ACCESS_CODE is not set, no access control is applied
74
  ACCESS_CODE = os.environ.get("ACCESS_CODE")
75
+ ENABLE_DYNAMIC_URLS = True
76
 
77
  # Get API key from environment - customizable variable name with validation
78
+ API_KEY = os.environ.get("API_KEY")
 
79
  if API_KEY:
80
  API_KEY = API_KEY.strip() # Remove any whitespace
81
  if not API_KEY: # Check if empty after stripping
 
86
  """Validate API key configuration with detailed logging"""
87
  if not API_KEY:
88
  print(f"⚠️ API KEY CONFIGURATION ERROR:")
89
+ print(f" Variable name: API_KEY")
90
  print(f" Status: Not set or empty")
91
+ print(f" Action needed: Set 'API_KEY' in HuggingFace Space secrets")
92
  print(f" Expected format: sk-or-xxxxxxxxxx")
93
  return False
94
  elif not API_KEY.startswith('sk-or-'):
95
  print(f"⚠️ API KEY FORMAT WARNING:")
96
+ print(f" Variable name: API_KEY")
97
  print(f" Current value: {API_KEY[:10]}..." if len(API_KEY) > 10 else API_KEY)
98
  print(f" Expected format: sk-or-xxxxxxxxxx")
99
  print(f" Note: OpenRouter keys should start with 'sk-or-'")
100
  return True # Still try to use it
101
  else:
102
  print(f"✅ API Key configured successfully")
103
+ print(f" Variable: API_KEY")
104
  print(f" Format: Valid OpenRouter key")
105
  return True
106
 
 
266
  error_msg += f"Please configure your OpenRouter API key:\n"
267
  error_msg += f"1. Go to Settings (⚙️) in your HuggingFace Space\n"
268
  error_msg += f"2. Click 'Variables and secrets'\n"
269
+ error_msg += f"3. Add secret: **API_KEY**\n"
270
  error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\n\n"
271
  error_msg += f"Get your API key at: https://openrouter.ai/keys"
272
+ print(f"❌ API request failed: No API key configured for API_KEY")
273
  return error_msg
274
 
275
  # Get grounding context
 
328
  "model": MODEL,
329
  "messages": messages,
330
  "temperature": 0.7,
331
+ "max_tokens": 750
332
  },
333
  timeout=30
334
  )
 
367
  error_msg = f"🔐 **Authentication Error**\n\n"
368
  error_msg += f"Your API key appears to be invalid or expired.\n\n"
369
  error_msg += f"**Troubleshooting:**\n"
370
+ error_msg += f"1. Check that your **API_KEY** secret is set correctly\n"
371
  error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\n"
372
  error_msg += f"3. Ensure your key starts with `sk-or-`\n"
373
  error_msg += f"4. Check that you have credits on your OpenRouter account"
 
442
  global _access_granted_global
443
  if ACCESS_CODE is None:
444
  _access_granted_global = True
445
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=True), gr.update(visible=False)
446
 
447
  if code == ACCESS_CODE:
448
  _access_granted_global = True
449
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=True), gr.update(visible=False)
450
  else:
451
  _access_granted_global = False
452
+ return gr.update(visible=True, value="❌ Incorrect access code. Please try again."), gr.update(visible=False), gr.update(value=False), gr.update(visible=True)
453
 
454
  def protected_generate_response(message, history):
455
  """Protected response function that checks access"""
 
513
 
514
  # Configuration status display
515
  def get_configuration_status():
516
+ """Generate a configuration status message for display"""
517
  status_parts = []
518
 
519
+ # API Key status
520
+ status_parts.append("### 🔑 API Configuration")
521
+ if API_KEY_VALID:
522
+ status_parts.append("**API Key:** Ready")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  else:
524
+ status_parts.append("**API Key:** Not configured")
525
+ status_parts.append(" Set `API_KEY` in Space secrets")
526
+
527
+ # Model and parameters
528
+ status_parts.append("") # Blank line
529
+ status_parts.append("### 🤖 Model Settings")
530
+ status_parts.append(f"**Model:** {MODEL.split('/')[-1]}")
531
+ status_parts.append(f"**Temperature:** 0.7")
532
+ status_parts.append(f"**Max Tokens:** 750")
533
 
534
  # URL Context if configured
535
+ if GROUNDING_URLS:
536
+ status_parts.append("") # Blank line
537
+ status_parts.append("### 🔗 Context Sources")
538
+ status_parts.append(f"**URLs Configured:** {len(GROUNDING_URLS)}")
539
+ for i, url in enumerate(GROUNDING_URLS[:2], 1):
540
+ status_parts.append(f" {i}. {url[:50]}{'...' if len(url) > 50 else ''}")
541
+ if len(GROUNDING_URLS) > 2:
542
+ status_parts.append(f" ... and {len(GROUNDING_URLS) - 2} more")
543
+
544
+ # Access control
545
+ if ACCESS_CODE is not None:
546
+ status_parts.append("") # Blank line
547
+ status_parts.append("### 🔐 Access Control")
548
+ status_parts.append("**Status:** Password protected")
549
 
550
+ # System prompt
551
+ status_parts.append("") # Blank line
552
+ status_parts.append("### 📝 System Prompt")
553
+ # Show first 200 chars of system prompt
554
+ prompt_preview = SYSTEM_PROMPT[:200] + "..." if len(SYSTEM_PROMPT) > 200 else SYSTEM_PROMPT
555
+ status_parts.append(f"```\n{prompt_preview}\n```")
556
 
557
  return "\n".join(status_parts)
558
 
 
582
  fn=store_and_generate_response, # Use wrapper function to store history
583
  title="", # Title already shown above
584
  description="", # Description already shown above
585
+ examples=EXAMPLES if EXAMPLES else None,
586
  type="messages" # Use modern message format for better compatibility
587
  )
588
 
 
597
  outputs=[export_file]
598
  )
599
 
 
 
 
600
 
601
  # Connect access verification
602
  if ACCESS_CODE is not None:
603
  access_btn.click(
604
  verify_access_code,
605
  inputs=[access_input],
606
+ outputs=[access_error, chat_section, access_granted, access_section]
607
  )
608
  access_input.submit(
609
  verify_access_code,
610
  inputs=[access_input],
611
+ outputs=[access_error, chat_section, access_granted, access_section]
612
  )
613
 
614
  # Faculty Configuration Section - appears at the bottom with password protection
615
+ with gr.Accordion("🔧 Faculty Configuration", open=False, visible=True) as faculty_section:
616
+ gr.Markdown("**Faculty Only:** Edit assistant configuration. Requires CONFIG_CODE secret.")
617
 
618
  # Check if faculty password is configured
619
  FACULTY_PASSWORD = os.environ.get("CONFIG_CODE", "").strip()
 
644
  current_config = json.load(f)
645
  except:
646
  current_config = {
647
+ 'name': SPACE_NAME.strip('"'),
648
+ 'description': SPACE_DESCRIPTION.strip('"'),
649
+ 'model': MODEL.strip('"'),
650
+ 'theme': THEME.strip('"'),
651
  'system_prompt': SYSTEM_PROMPT,
652
+ 'examples': [],
653
+ 'grounding_urls': GROUNDING_URLS,
654
  'temperature': 0.7,
655
+ 'max_tokens': 750,
656
  'locked': False
657
  }
658
 
659
  # Editable fields
660
+ edit_name = gr.Textbox(
661
+ label="Assistant Name",
662
+ value=current_config.get('name', SPACE_NAME.strip('"')),
663
+ info="The display name for your assistant"
664
+ )
665
+
666
+ edit_description = gr.Textbox(
667
+ label="Assistant Description",
668
+ value=current_config.get('description', SPACE_DESCRIPTION.strip('"')),
669
+ lines=2,
670
+ info="Brief description of what your assistant does"
671
+ )
672
+
673
+ edit_model = gr.Dropdown(
674
+ label="AI Model",
675
+ choices=[
676
+ "google/gemini-2.0-flash-001",
677
+ "google/gemma-3-27b-it",
678
+ "anthropic/claude-3.5-sonnet",
679
+ "anthropic/claude-3.5-haiku",
680
+ "openai/gpt-4o-mini-search-preview",
681
+ "openai/gpt-4.1-nano",
682
+ "nvidia/llama-3.1-nemotron-70b-instruct",
683
+ "mistralai/devstral-small"
684
+ ],
685
+ value=current_config.get('model', MODEL.strip('"')),
686
+ info="The AI model to use for responses"
687
+ )
688
+
689
+ edit_theme = gr.Dropdown(
690
+ label="Interface Theme",
691
+ choices=["Default", "Origin", "Citrus", "Monochrome", "Soft", "Glass", "Ocean"],
692
+ value=current_config.get('theme', THEME.strip('"')),
693
+ info="Visual theme for the chat interface"
694
+ )
695
+
696
  edit_system_prompt = gr.Textbox(
697
  label="System Prompt",
698
  value=current_config.get('system_prompt', SYSTEM_PROMPT),
699
  lines=5
700
  )
701
 
702
+ # Get current examples as text
703
+ current_examples = current_config.get('examples', [])
704
+ examples_text_value = "\n".join(current_examples) if isinstance(current_examples, list) else ""
 
 
 
705
 
706
  edit_examples = gr.Textbox(
707
  label="Example Prompts (one per line)",
 
722
  label="Max Tokens",
723
  minimum=50,
724
  maximum=4096,
725
+ value=current_config.get('max_tokens', 750),
726
  step=50
727
  )
728
 
729
  # URL Grounding fields
730
+ gr.Markdown("### 🔗 URL Context")
731
+ gr.Markdown("**Primary URLs** (processed first, highest priority)")
732
+
733
+ # Get current URLs from config
734
+ current_urls = current_config.get('grounding_urls', GROUNDING_URLS or [])
735
+ if isinstance(current_urls, str):
736
  try:
737
  import ast
738
+ current_urls = ast.literal_eval(current_urls)
739
  except:
740
+ current_urls = []
741
 
742
+ edit_url1 = gr.Textbox(
743
+ label="Primary URL 1",
744
+ value=current_urls[0] if len(current_urls) > 0 else "",
745
+ placeholder="https://..."
746
+ )
747
+ edit_url2 = gr.Textbox(
748
+ label="Primary URL 2",
749
+ value=current_urls[1] if len(current_urls) > 1 else "",
750
+ placeholder="https://..."
751
+ )
752
+
753
+ gr.Markdown("**Secondary URLs** (additional context)")
754
+ edit_url3 = gr.Textbox(label="Secondary URL 1", value=current_urls[2] if len(current_urls) > 2 else "", placeholder="https://...")
755
+ edit_url4 = gr.Textbox(label="Secondary URL 2", value=current_urls[3] if len(current_urls) > 3 else "", placeholder="https://...")
756
+ edit_url5 = gr.Textbox(label="Secondary URL 3", value=current_urls[4] if len(current_urls) > 4 else "", placeholder="https://...")
757
+ edit_url6 = gr.Textbox(label="Secondary URL 4", value=current_urls[5] if len(current_urls) > 5 else "", placeholder="https://...")
758
+ edit_url7 = gr.Textbox(label="Secondary URL 5", value=current_urls[6] if len(current_urls) > 6 else "", placeholder="https://...")
759
+ edit_url8 = gr.Textbox(label="Secondary URL 6", value=current_urls[7] if len(current_urls) > 7 else "", placeholder="https://...")
760
+ edit_url9 = gr.Textbox(label="Secondary URL 7", value=current_urls[8] if len(current_urls) > 8 else "", placeholder="https://...")
761
+ edit_url10 = gr.Textbox(label="Secondary URL 8", value=current_urls[9] if len(current_urls) > 9 else "", placeholder="https://...")
762
 
763
  config_locked = gr.Checkbox(
764
  label="Lock Configuration (Prevent further edits)",
 
766
  )
767
 
768
  with gr.Row():
769
+ save_config_btn = gr.Button("💾 Save Configuration", variant="primary")
770
+ reset_config_btn = gr.Button("↩️ Reset to Defaults", variant="secondary")
771
 
772
  config_status = gr.Markdown("")
773
 
 
775
  def verify_faculty_password(password):
776
  if password == FACULTY_PASSWORD:
777
  return (
778
+ gr.update(value="Authentication successful!"),
779
  gr.update(visible=False), # Hide auth row
780
  gr.update(visible=True), # Show config section
781
  True # Update auth state
782
  )
783
  else:
784
  return (
785
+ gr.update(value="Invalid password"),
786
  gr.update(visible=True), # Keep auth row visible
787
  gr.update(visible=False), # Keep config hidden
788
  False # Auth failed
789
  )
790
 
791
  # Save configuration function
792
+ def save_configuration(new_name, new_description, new_model, new_theme, new_prompt, new_examples, new_temp, new_tokens, url1, url2, url3, url4, url5, url6, url7, url8, url9, url10, lock_config, is_authenticated):
793
  if not is_authenticated:
794
+ # Return not authenticated message and keep current values
795
+ return (
796
+ "❌ Not authenticated",
797
+ gr.update(), # Keep current name
798
+ gr.update(), # Keep current description
799
+ gr.update(), # Keep current model
800
+ gr.update(), # Keep current theme
801
+ gr.update(), # Keep current prompt
802
+ gr.update(), # Keep current examples
803
+ gr.update(), # Keep current temp
804
+ gr.update(), # Keep current tokens
805
+ gr.update(), # Keep current url1
806
+ gr.update(), # Keep current url2
807
+ gr.update(), # Keep current url3
808
+ gr.update(), # Keep current url4
809
+ gr.update(), # Keep current url5
810
+ gr.update(), # Keep current url6
811
+ gr.update(), # Keep current url7
812
+ gr.update(), # Keep current url8
813
+ gr.update(), # Keep current url9
814
+ gr.update(), # Keep current url10
815
+ gr.update() # Keep current lock
816
+ )
817
 
818
  # Check if configuration is already locked
819
  try:
820
  with open('config.json', 'r') as f:
821
  existing_config = json.load(f)
822
  if existing_config.get('locked', False):
823
+ # Return locked message and keep current values
824
+ return (
825
+ "🔒 Configuration is locked and cannot be modified",
826
+ gr.update(), # Keep current name
827
+ gr.update(), # Keep current description
828
+ gr.update(), # Keep current model
829
+ gr.update(), # Keep current theme
830
+ gr.update(), # Keep current prompt
831
+ gr.update(), # Keep current examples
832
+ gr.update(), # Keep current temp
833
+ gr.update(), # Keep current tokens
834
+ gr.update(), # Keep current url1
835
+ gr.update(), # Keep current url2
836
+ gr.update(), # Keep current url3
837
+ gr.update(), # Keep current url4
838
+ gr.update(), # Keep current url5
839
+ gr.update(), # Keep current url6
840
+ gr.update(), # Keep current url7
841
+ gr.update(), # Keep current url8
842
+ gr.update(), # Keep current url9
843
+ gr.update(), # Keep current url10
844
+ gr.update() # Keep current lock
845
+ )
846
  except:
847
  pass
848
 
849
+ # Process examples
 
 
 
 
 
 
 
 
850
  examples_list = [ex.strip() for ex in new_examples.split('\n') if ex.strip()]
851
 
852
+ # Process URLs
853
+ urls = [url1, url2, url3, url4, url5, url6, url7, url8, url9, url10]
 
 
854
  grounding_urls = [url.strip() for url in urls if url.strip()]
855
 
856
+ # Save new configuration
857
+ new_config = {
858
+ 'name': new_name,
859
+ 'description': new_description,
860
+ 'model': new_model,
861
+ 'theme': new_theme,
862
  'system_prompt': new_prompt,
863
  'examples': examples_list,
864
+ 'grounding_urls': grounding_urls,
865
  'temperature': new_temp,
866
  'max_tokens': int(new_tokens),
867
+ 'enable_dynamic_urls': True,
868
  'locked': lock_config,
869
  'last_modified': datetime.now().isoformat(),
870
  'modified_by': 'faculty'
871
+ }
872
 
873
  try:
874
  with open('config.json', 'w') as f:
875
+ json.dump(new_config, f, indent=2)
876
+ print(f"✅ Faculty config saved to config.json: {new_config}")
877
 
878
+ # Update global variables
879
+ global SPACE_NAME, SPACE_DESCRIPTION, MODEL, THEME, SYSTEM_PROMPT, temperature, max_tokens, GROUNDING_URLS, EXAMPLES
880
+ SPACE_NAME = new_name
881
+ SPACE_DESCRIPTION = new_description
882
+ MODEL = new_model
883
+ THEME = new_theme
884
+ SYSTEM_PROMPT = new_prompt
885
+ temperature = new_temp
886
+ max_tokens = int(new_tokens)
887
+ GROUNDING_URLS = grounding_urls
888
+ EXAMPLES = examples_list
889
 
890
+ # Return success message AND updated form values
891
+ success_msg = f"✅ Configuration saved successfully at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n🔄 **Please refresh the page to see all changes take effect.**"
892
+
893
+ # Return tuple with status message and all updated field values
894
+ return (
895
+ success_msg,
896
+ gr.update(value=new_name),
897
+ gr.update(value=new_description),
898
+ gr.update(value=new_model),
899
+ gr.update(value=new_theme),
900
+ gr.update(value=new_prompt),
901
+ gr.update(value=new_examples),
902
+ gr.update(value=new_temp),
903
+ gr.update(value=int(new_tokens)),
904
+ gr.update(value=url1),
905
+ gr.update(value=url2),
906
+ gr.update(value=url3),
907
+ gr.update(value=url4),
908
+ gr.update(value=url5),
909
+ gr.update(value=url6),
910
+ gr.update(value=url7),
911
+ gr.update(value=url8),
912
+ gr.update(value=url9),
913
+ gr.update(value=url10),
914
+ gr.update(value=lock_config)
915
+ )
916
  except Exception as e:
917
+ error_msg = f"Error saving configuration: {str(e)}"
918
+ # Return error message and keep current values
919
+ return (
920
+ error_msg,
921
+ gr.update(), # Keep current name
922
+ gr.update(), # Keep current description
923
+ gr.update(), # Keep current model
924
+ gr.update(), # Keep current theme
925
+ gr.update(), # Keep current prompt
926
+ gr.update(), # Keep current examples
927
+ gr.update(), # Keep current temp
928
+ gr.update(), # Keep current tokens
929
+ gr.update(), # Keep current url1
930
+ gr.update(), # Keep current url2
931
+ gr.update(), # Keep current url3
932
+ gr.update(), # Keep current url4
933
+ gr.update(), # Keep current url5
934
+ gr.update(), # Keep current url6
935
+ gr.update(), # Keep current url7
936
+ gr.update(), # Keep current url8
937
+ gr.update(), # Keep current url9
938
+ gr.update(), # Keep current url10
939
+ gr.update() # Keep current lock
940
+ )
941
 
942
  # Reset configuration function
943
  def reset_configuration(is_authenticated):
944
  if not is_authenticated:
945
+ updates = ["Not authenticated"] + [gr.update() for _ in range(17)] # 1 status + 16 fields
946
  return tuple(updates)
947
 
948
  # Check if locked
 
950
  with open('config.json', 'r') as f:
951
  existing_config = json.load(f)
952
  if existing_config.get('locked', False):
953
+ updates = ["🔒 Configuration is locked"] + [gr.update() for _ in range(17)]
954
  return tuple(updates)
955
  except:
956
  pass
957
 
958
+ # Reset to original values
959
+ return (
960
+ "↩️ Reset to default values",
961
+ gr.update(value=SPACE_NAME.strip('"')), # name
962
+ gr.update(value=SPACE_DESCRIPTION.strip('"')), # description
963
+ gr.update(value=MODEL.strip('"')), # model
964
+ gr.update(value=THEME.strip('"')), # theme
965
+ gr.update(value=SYSTEM_PROMPT), # system prompt
966
+ gr.update(value=""), # examples
967
+ gr.update(value=0.7), # temperature
968
+ gr.update(value=750), # max tokens
969
+ gr.update(value=""), # url1
970
+ gr.update(value=""), # url2
971
+ gr.update(value=""), # url3
972
+ gr.update(value=""), # url4
973
+ gr.update(value=""), # url5
974
+ gr.update(value=""), # url6
975
+ gr.update(value=""), # url7
976
+ gr.update(value=""), # url8
977
+ gr.update(value=""), # url9
978
+ gr.update(value="") # url10
979
+ )
 
 
 
 
 
 
 
 
 
980
 
981
  # Connect authentication
982
  faculty_auth_btn.click(
 
994
  # Connect configuration buttons
995
  save_config_btn.click(
996
  save_configuration,
997
+ inputs=[edit_name, edit_description, edit_model, edit_theme, edit_system_prompt, edit_examples, edit_temperature, edit_max_tokens,
998
+ edit_url1, edit_url2, edit_url3, edit_url4, edit_url5, edit_url6, edit_url7, edit_url8, edit_url9, edit_url10,
999
+ config_locked, faculty_auth_state],
1000
+ outputs=[config_status, edit_name, edit_description, edit_model, edit_theme, edit_system_prompt, edit_examples, edit_temperature, edit_max_tokens,
1001
+ edit_url1, edit_url2, edit_url3, edit_url4, edit_url5, edit_url6, edit_url7, edit_url8, edit_url9, edit_url10, config_locked]
1002
  )
1003
 
1004
  reset_config_btn.click(
1005
  reset_configuration,
1006
  inputs=[faculty_auth_state],
1007
+ outputs=[config_status, edit_name, edit_description, edit_model, edit_theme, edit_system_prompt, edit_examples, edit_temperature, edit_max_tokens,
1008
+ edit_url1, edit_url2, edit_url3, edit_url4, edit_url5, edit_url6, edit_url7, edit_url8, edit_url9, edit_url10]
1009
  )
1010
  else:
1011
+ gr.Markdown("ℹ️ Faculty configuration is not enabled. Set CONFIG_CODE in Space secrets to enable.")
1012
 
1013
  if __name__ == "__main__":
1014
  demo.launch()
config.json CHANGED
@@ -1,13 +1,18 @@
1
  {
2
  "name": "AI Assistant",
3
  "description": "A customizable AI assistant",
4
- "system_prompt": "You are an expert literary analysis instructor specializing in close reading techniques across genres, periods, and theoretical approaches. Your role is to help students develop sophisticated reading practices connecting textual details to interpretive arguments. Follow the close reading framework below:\n\n1. Reading Process\n1a. Initial Analysis: ensure literal comprehension; identify literary devices and effects; examine diction, syntax, imagery\n1b. Pattern Recognition: trace recurring motifs, tensions, contradictions; analyze formal elements by genre; connect style to meaning\n1c. Interpretive Synthesis: apply theoretical lenses when appropriate; build arguments through textual evidence; acknowledge interpretive complexity\n\n2. Genre-Specific Techniques:\n2a. Poetry: prosody, enjambment, sound patterns, visual arrangement\n2b. Fiction: narration, characterization, setting, temporal structure\n2c. Drama: dialogue patterns, stage directions, performance considerations\n\n3. Critical Approaches:\n3a. **Formal Analysis**: unity, ambiguity, paradox, irony\n3b. **Contextual Reading**: historical, biographical, cultural frameworks\n3c. **Theoretical Applications**: psychoanalytic, Marxist, feminist, postcolonial",
5
- "model": "nvidia/llama-3.1-nemotron-70b-instruct",
6
  "api_key_var": "API_KEY",
7
  "temperature": 0.7,
8
- "max_tokens": 1500,
9
- "examples": "['How do I analyze symbolism in this passage?', 'Help me apply feminist theory to this text', 'What should I look for in poetry analysis?', 'How do I write a literary analysis paper?']",
10
- "grounding_urls": "[\"https://owl.purdue.edu/owl/subject_specific_writing/writing_in_literature/writing_about_fiction/index.html\", \"https://writingcenter.fas.harvard.edu/pages/how-do-close-reading\"]",
 
 
 
 
11
  "enable_dynamic_urls": true,
12
- "theme": "Origin"
 
13
  }
 
1
  {
2
  "name": "AI Assistant",
3
  "description": "A customizable AI assistant",
4
+ "system_prompt": "You are an AI assistant specialized in mathematics and statistics who guides users through problem-solving rather than providing direct answers. You help users discover solutions by asking strategic questions ('What do we know so far?' 'What method might apply here?' 'Can you identify a pattern?'), prompting them to explain their reasoning, and offering hints that build on their current understanding. Format all mathematical expressions in LaTeX (inline: $x^2 + y^2 = r^2$, display: $$\\int_a^b f(x)dx$$). When users are stuck, provide scaffolded support: suggest examining simpler cases, identifying relevant formulas or theorems, or breaking the problem into smaller parts. Use multiple representations to illuminate different aspects of the problem, validate partial progress to build confidence, and help users recognize and correct their own errors through targeted questions rather than corrections. Your goal is to develop problem-solving skills and mathematical reasoning, not just arrive at answers.",
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
+ "Hello! How can you help me?",
11
+ "Tell me something interesting",
12
+ "What can you do?"
13
+ ],
14
+ "grounding_urls": [],
15
  "enable_dynamic_urls": true,
16
+ "theme": "Default",
17
+ "locked": false
18
  }