mnadell commited on
Commit
2a9d4e1
Β·
verified Β·
1 Parent(s): a8ba6a4

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +562 -0
  2. config.json +12 -0
  3. requirements.txt +4 -0
app.py ADDED
@@ -0,0 +1,562 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import tempfile
3
+ import os
4
+ import requests
5
+ import json
6
+ import re
7
+ from bs4 import BeautifulSoup
8
+ from datetime import datetime
9
+ import urllib.parse
10
+
11
+
12
+ # Configuration
13
+ SPACE_NAME = "Writing Process 101: Brainstorming"
14
+ SPACE_DESCRIPTION = "A place to help students free write and brainstorm."
15
+ SYSTEM_PROMPT = """You are a pedagogically-minded academic assistant designed for an advanced elective for undergraduate English majors. The class is about the relationship between history and literature, about how to approach historical questions with a literary approach and approach literary questions with an historical approach. Specifically, the class is focusing on the history and dedication of Red Hook in the 1970s, circulating around novels, short stories, and other narratives (not just fictional) related to Red Hook. Your approach follows constructivist learning principles: build on students' prior knowledge, scaffold complex concepts through graduated questioning, and use Socratic dialogue to guide discovery. When students pose a question or raise an idea, respond with questions. Each question should model critical thinking by acknowledging multiple perspectives, identifying assumptions, and revealing conceptual relationships. Conclude with open-ended questions that promote higher-order thinkingβ€”analysis, synthesis, or evaluationβ€”rather than recall.
16
+
17
+ Use a warm tone, encourage engagement, reflect on what the students bring to this process that AI cannot. Develop exercises to help students brainstorm."""
18
+ MODEL = "anthropic/claude-3.5-haiku"
19
+ GROUNDING_URLS = []
20
+ # Get access code from environment variable for security
21
+ # If SPACE_ACCESS_CODE is not set, no access control is applied
22
+ ACCESS_CODE = os.environ.get("SPACE_ACCESS_CODE")
23
+ ENABLE_DYNAMIC_URLS = False
24
+
25
+ # Get API key from environment - customizable variable name with validation
26
+ API_KEY = os.environ.get("OPENROUTER_API_KEY")
27
+ if API_KEY:
28
+ API_KEY = API_KEY.strip() # Remove any whitespace
29
+ if not API_KEY: # Check if empty after stripping
30
+ API_KEY = None
31
+
32
+ # API Key validation and logging
33
+ def validate_api_key():
34
+ """Validate API key configuration with detailed logging"""
35
+ if not API_KEY:
36
+ print(f"⚠️ API KEY CONFIGURATION ERROR:")
37
+ print(f" Variable name: OPENROUTER_API_KEY")
38
+ print(f" Status: Not set or empty")
39
+ print(f" Action needed: Set 'OPENROUTER_API_KEY' in HuggingFace Space secrets")
40
+ print(f" Expected format: sk-or-xxxxxxxxxx")
41
+ return False
42
+ elif not API_KEY.startswith('sk-or-'):
43
+ print(f"⚠️ API KEY FORMAT WARNING:")
44
+ print(f" Variable name: OPENROUTER_API_KEY")
45
+ print(f" Current value: {API_KEY[:10]}..." if len(API_KEY) > 10 else API_KEY)
46
+ print(f" Expected format: sk-or-xxxxxxxxxx")
47
+ print(f" Note: OpenRouter keys should start with 'sk-or-'")
48
+ return True # Still try to use it
49
+ else:
50
+ print(f"βœ… API Key configured successfully")
51
+ print(f" Variable: OPENROUTER_API_KEY")
52
+ print(f" Format: Valid OpenRouter key")
53
+ return True
54
+
55
+ # Validate on startup
56
+ try:
57
+ API_KEY_VALID = validate_api_key()
58
+ except NameError:
59
+ # During template generation, API_KEY might not be defined yet
60
+ API_KEY_VALID = False
61
+
62
+ def validate_url_domain(url):
63
+ """Basic URL domain validation"""
64
+ try:
65
+ from urllib.parse import urlparse
66
+ parsed = urlparse(url)
67
+ # Check for valid domain structure
68
+ if parsed.netloc and '.' in parsed.netloc:
69
+ return True
70
+ except:
71
+ pass
72
+ return False
73
+
74
+ def fetch_url_content(url):
75
+ """Enhanced URL content fetching with improved compatibility and error handling"""
76
+ if not validate_url_domain(url):
77
+ return f"Invalid URL format: {url}"
78
+
79
+ try:
80
+ # Enhanced headers for better compatibility
81
+ headers = {
82
+ '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',
83
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
84
+ 'Accept-Language': 'en-US,en;q=0.5',
85
+ 'Accept-Encoding': 'gzip, deflate',
86
+ 'Connection': 'keep-alive'
87
+ }
88
+
89
+ response = requests.get(url, timeout=15, headers=headers)
90
+ response.raise_for_status()
91
+ soup = BeautifulSoup(response.content, 'html.parser')
92
+
93
+ # Enhanced content cleaning
94
+ for element in soup(["script", "style", "nav", "header", "footer", "aside", "form", "button"]):
95
+ element.decompose()
96
+
97
+ # Extract main content preferentially
98
+ 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
99
+ text = main_content.get_text()
100
+
101
+ # Enhanced text cleaning
102
+ lines = (line.strip() for line in text.splitlines())
103
+ chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
104
+ text = ' '.join(chunk for chunk in chunks if chunk and len(chunk) > 2)
105
+
106
+ # Smart truncation - try to end at sentence boundaries
107
+ if len(text) > 4000:
108
+ truncated = text[:4000]
109
+ last_period = truncated.rfind('.')
110
+ if last_period > 3000: # If we can find a reasonable sentence break
111
+ text = truncated[:last_period + 1]
112
+ else:
113
+ text = truncated + "..."
114
+
115
+ return text if text.strip() else "No readable content found at this URL"
116
+
117
+ except requests.exceptions.Timeout:
118
+ return f"Timeout error fetching {url} (15s limit exceeded)"
119
+ except requests.exceptions.RequestException as e:
120
+ return f"Error fetching {url}: {str(e)}"
121
+ except Exception as e:
122
+ return f"Error processing content from {url}: {str(e)}"
123
+
124
+ def extract_urls_from_text(text):
125
+ """Extract URLs from text using regex with enhanced validation"""
126
+ import re
127
+ url_pattern = r'https?://[^\s<>"{}|\\^`\[\]"]+'
128
+ urls = re.findall(url_pattern, text)
129
+
130
+ # Basic URL validation and cleanup
131
+ validated_urls = []
132
+ for url in urls:
133
+ # Remove trailing punctuation that might be captured
134
+ url = url.rstrip('.,!?;:')
135
+ # Basic domain validation
136
+ if '.' in url and len(url) > 10:
137
+ validated_urls.append(url)
138
+
139
+ return validated_urls
140
+
141
+ # Global cache for URL content to avoid re-crawling in generated spaces
142
+ _url_content_cache = {}
143
+
144
+ def get_grounding_context():
145
+ """Fetch context from grounding URLs with caching"""
146
+ if not GROUNDING_URLS:
147
+ return ""
148
+
149
+ # Create cache key from URLs
150
+ cache_key = tuple(sorted([url for url in GROUNDING_URLS if url and url.strip()]))
151
+
152
+ # Check cache first
153
+ if cache_key in _url_content_cache:
154
+ return _url_content_cache[cache_key]
155
+
156
+ context_parts = []
157
+ for i, url in enumerate(GROUNDING_URLS, 1):
158
+ if url.strip():
159
+ content = fetch_url_content(url.strip())
160
+ # Add priority indicators
161
+ priority_label = "PRIMARY" if i <= 2 else "SECONDARY"
162
+ context_parts.append(f"[{priority_label}] Context from URL {i} ({url}):\n{content}")
163
+
164
+ if context_parts:
165
+ result = "\n\n" + "\n\n".join(context_parts) + "\n\n"
166
+ else:
167
+ result = ""
168
+
169
+ # Cache the result
170
+ _url_content_cache[cache_key] = result
171
+ return result
172
+
173
+ def export_conversation_to_markdown(conversation_history):
174
+ """Export conversation history to markdown format"""
175
+ if not conversation_history:
176
+ return "No conversation to export."
177
+
178
+ markdown_content = f"""# Conversation Export
179
+ Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
180
+
181
+ ---
182
+
183
+ """
184
+
185
+ message_pair_count = 0
186
+ for i, message in enumerate(conversation_history):
187
+ if isinstance(message, dict):
188
+ role = message.get('role', 'unknown')
189
+ content = message.get('content', '')
190
+
191
+ if role == 'user':
192
+ message_pair_count += 1
193
+ markdown_content += f"## User Message {message_pair_count}\n\n{content}\n\n"
194
+ elif role == 'assistant':
195
+ markdown_content += f"## Assistant Response {message_pair_count}\n\n{content}\n\n---\n\n"
196
+ elif isinstance(message, (list, tuple)) and len(message) >= 2:
197
+ # Handle legacy tuple format: ["user msg", "assistant msg"]
198
+ message_pair_count += 1
199
+ user_msg, assistant_msg = message[0], message[1]
200
+ if user_msg:
201
+ markdown_content += f"## User Message {message_pair_count}\n\n{user_msg}\n\n"
202
+ if assistant_msg:
203
+ markdown_content += f"## Assistant Response {message_pair_count}\n\n{assistant_msg}\n\n---\n\n"
204
+
205
+ return markdown_content
206
+
207
+
208
+ def generate_response(message, history):
209
+ """Generate response using OpenRouter API"""
210
+
211
+ # Enhanced API key validation with helpful messages
212
+ if not API_KEY:
213
+ error_msg = f"πŸ”‘ **API Key Required**\n\n"
214
+ error_msg += f"Please configure your OpenRouter API key:\n"
215
+ error_msg += f"1. Go to Settings (βš™οΈ) in your HuggingFace Space\n"
216
+ error_msg += f"2. Click 'Variables and secrets'\n"
217
+ error_msg += f"3. Add secret: **OPENROUTER_API_KEY**\n"
218
+ error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\n\n"
219
+ error_msg += f"Get your API key at: https://openrouter.ai/keys"
220
+ print(f"❌ API request failed: No API key configured for OPENROUTER_API_KEY")
221
+ return error_msg
222
+
223
+ # Get grounding context
224
+ grounding_context = get_grounding_context()
225
+
226
+
227
+ # If dynamic URLs are enabled, check message for URLs to fetch
228
+ if ENABLE_DYNAMIC_URLS:
229
+ urls_in_message = extract_urls_from_text(message)
230
+ if urls_in_message:
231
+ # Fetch content from URLs mentioned in the message
232
+ dynamic_context_parts = []
233
+ for url in urls_in_message[:3]: # Limit to 3 URLs per message
234
+ content = fetch_url_content(url)
235
+ dynamic_context_parts.append(f"\n\nDynamic context from {url}:\n{content}")
236
+ if dynamic_context_parts:
237
+ grounding_context += "\n".join(dynamic_context_parts)
238
+
239
+ # Build enhanced system prompt with grounding context
240
+ enhanced_system_prompt = SYSTEM_PROMPT + grounding_context
241
+
242
+ # Build messages array for the API
243
+ messages = [{"role": "system", "content": enhanced_system_prompt}]
244
+
245
+ # Add conversation history - handle both modern messages format and legacy tuples
246
+ for chat in history:
247
+ if isinstance(chat, dict):
248
+ # Modern format: {"role": "user", "content": "..."} or {"role": "assistant", "content": "..."}
249
+ messages.append(chat)
250
+ elif isinstance(chat, (list, tuple)) and len(chat) >= 2:
251
+ # Legacy format: ["user msg", "assistant msg"] or ("user msg", "assistant msg")
252
+ user_msg, assistant_msg = chat[0], chat[1]
253
+ if user_msg:
254
+ messages.append({"role": "user", "content": user_msg})
255
+ if assistant_msg:
256
+ messages.append({"role": "assistant", "content": assistant_msg})
257
+
258
+ # Add current message
259
+ messages.append({"role": "user", "content": message})
260
+
261
+ # Make API request with enhanced error handling
262
+ try:
263
+ print(f"πŸ”„ Making API request to OpenRouter...")
264
+ print(f" Model: {MODEL}")
265
+ print(f" Messages: {len(messages)} in conversation")
266
+
267
+ response = requests.post(
268
+ url="https://openrouter.ai/api/v1/chat/completions",
269
+ headers={
270
+ "Authorization": f"Bearer {API_KEY}",
271
+ "Content-Type": "application/json",
272
+ "HTTP-Referer": "https://huggingface.co", # Required by some providers
273
+ "X-Title": "HuggingFace Space" # Helpful for tracking
274
+ },
275
+ json={
276
+ "model": MODEL,
277
+ "messages": messages,
278
+ "temperature": 0.7,
279
+ "max_tokens": 750
280
+ },
281
+ timeout=30
282
+ )
283
+
284
+ print(f"πŸ“‘ API Response: {response.status_code}")
285
+
286
+ if response.status_code == 200:
287
+ try:
288
+ result = response.json()
289
+
290
+ # Enhanced validation of API response structure
291
+ if 'choices' not in result or not result['choices']:
292
+ print(f"⚠️ API response missing choices: {result}")
293
+ return "API Error: No response choices available"
294
+ elif 'message' not in result['choices'][0]:
295
+ print(f"⚠️ API response missing message: {result}")
296
+ return "API Error: No message in response"
297
+ elif 'content' not in result['choices'][0]['message']:
298
+ print(f"⚠️ API response missing content: {result}")
299
+ return "API Error: No content in message"
300
+ else:
301
+ content = result['choices'][0]['message']['content']
302
+
303
+ # Check for empty content
304
+ if not content or content.strip() == "":
305
+ print(f"⚠️ API returned empty content")
306
+ return "API Error: Empty response content"
307
+
308
+ print(f"βœ… API request successful")
309
+ return content
310
+
311
+ except (KeyError, IndexError, json.JSONDecodeError) as e:
312
+ print(f"❌ Failed to parse API response: {str(e)}")
313
+ return f"API Error: Failed to parse response - {str(e)}"
314
+ elif response.status_code == 401:
315
+ error_msg = f"πŸ” **Authentication Error**\n\n"
316
+ error_msg += f"Your API key appears to be invalid or expired.\n\n"
317
+ error_msg += f"**Troubleshooting:**\n"
318
+ error_msg += f"1. Check that your **OPENROUTER_API_KEY** secret is set correctly\n"
319
+ error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\n"
320
+ error_msg += f"3. Ensure your key starts with `sk-or-`\n"
321
+ error_msg += f"4. Check that you have credits on your OpenRouter account"
322
+ print(f"❌ API authentication failed: {response.status_code} - {response.text[:200]}")
323
+ return error_msg
324
+ elif response.status_code == 429:
325
+ error_msg = f"⏱️ **Rate Limit Exceeded**\n\n"
326
+ error_msg += f"Too many requests. Please wait a moment and try again.\n\n"
327
+ error_msg += f"**Troubleshooting:**\n"
328
+ error_msg += f"1. Wait 30-60 seconds before trying again\n"
329
+ error_msg += f"2. Check your OpenRouter usage limits\n"
330
+ error_msg += f"3. Consider upgrading your OpenRouter plan"
331
+ print(f"❌ Rate limit exceeded: {response.status_code}")
332
+ return error_msg
333
+ elif response.status_code == 400:
334
+ try:
335
+ error_data = response.json()
336
+ error_message = error_data.get('error', {}).get('message', 'Unknown error')
337
+ except:
338
+ error_message = response.text
339
+
340
+ error_msg = f"⚠️ **Request Error**\n\n"
341
+ error_msg += f"The API request was invalid:\n"
342
+ error_msg += f"`{error_message}`\n\n"
343
+ if "model" in error_message.lower():
344
+ error_msg += f"**Model Issue:** The model `{MODEL}` may not be available.\n"
345
+ error_msg += f"Try switching to a different model in your Space configuration."
346
+ print(f"❌ Bad request: {response.status_code} - {error_message}")
347
+ return error_msg
348
+ else:
349
+ error_msg = f"🚫 **API Error {response.status_code}**\n\n"
350
+ error_msg += f"An unexpected error occurred. Please try again.\n\n"
351
+ error_msg += f"If this persists, check:\n"
352
+ error_msg += f"1. OpenRouter service status\n"
353
+ error_msg += f"2. Your API key and credits\n"
354
+ error_msg += f"3. The model availability"
355
+ print(f"❌ API error: {response.status_code} - {response.text[:200]}")
356
+ return error_msg
357
+
358
+ except requests.exceptions.Timeout:
359
+ error_msg = f"⏰ **Request Timeout**\n\n"
360
+ error_msg += f"The API request took too long (30s limit).\n\n"
361
+ error_msg += f"**Troubleshooting:**\n"
362
+ error_msg += f"1. Try again with a shorter message\n"
363
+ error_msg += f"2. Check your internet connection\n"
364
+ error_msg += f"3. Try a different model"
365
+ print(f"❌ Request timeout after 30 seconds")
366
+ return error_msg
367
+ except requests.exceptions.ConnectionError:
368
+ error_msg = f"🌐 **Connection Error**\n\n"
369
+ error_msg += f"Could not connect to OpenRouter API.\n\n"
370
+ error_msg += f"**Troubleshooting:**\n"
371
+ error_msg += f"1. Check your internet connection\n"
372
+ error_msg += f"2. Check OpenRouter service status\n"
373
+ error_msg += f"3. Try again in a few moments"
374
+ print(f"❌ Connection error to OpenRouter API")
375
+ return error_msg
376
+ except Exception as e:
377
+ error_msg = f"❌ **Unexpected Error**\n\n"
378
+ error_msg += f"An unexpected error occurred:\n"
379
+ error_msg += f"`{str(e)}`\n\n"
380
+ error_msg += f"Please try again or contact support if this persists."
381
+ print(f"❌ Unexpected error: {str(e)}")
382
+ return error_msg
383
+
384
+ # Access code verification
385
+ access_granted = gr.State(False)
386
+ _access_granted_global = False # Global fallback
387
+
388
+ def verify_access_code(code):
389
+ """Verify the access code"""
390
+ global _access_granted_global
391
+ if ACCESS_CODE is None:
392
+ _access_granted_global = True
393
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=True)
394
+
395
+ if code == ACCESS_CODE:
396
+ _access_granted_global = True
397
+ return gr.update(visible=False), gr.update(visible=True), gr.update(value=True)
398
+ else:
399
+ _access_granted_global = False
400
+ return gr.update(visible=True, value="❌ Incorrect access code. Please try again."), gr.update(visible=False), gr.update(value=False)
401
+
402
+ def protected_generate_response(message, history):
403
+ """Protected response function that checks access"""
404
+ # Check if access is granted via the global variable
405
+ if ACCESS_CODE is not None and not _access_granted_global:
406
+ return "Please enter the access code to continue."
407
+ return generate_response(message, history)
408
+
409
+ # Global variable to store chat history for export
410
+ chat_history_store = []
411
+
412
+ def store_and_generate_response(message, history):
413
+ """Wrapper function that stores history and generates response"""
414
+ global chat_history_store
415
+
416
+ # Generate response using the protected function
417
+ response = protected_generate_response(message, history)
418
+
419
+ # Convert current history to the format we need for export
420
+ # history comes in as [["user1", "bot1"], ["user2", "bot2"], ...]
421
+ chat_history_store = []
422
+ if history:
423
+ for exchange in history:
424
+ if isinstance(exchange, (list, tuple)) and len(exchange) >= 2:
425
+ chat_history_store.append({"role": "user", "content": exchange[0]})
426
+ chat_history_store.append({"role": "assistant", "content": exchange[1]})
427
+
428
+ # Add the current exchange
429
+ chat_history_store.append({"role": "user", "content": message})
430
+ chat_history_store.append({"role": "assistant", "content": response})
431
+
432
+ return response
433
+
434
+ def export_current_conversation():
435
+ """Export the current conversation"""
436
+ if not chat_history_store:
437
+ return gr.update(visible=False)
438
+
439
+ markdown_content = export_conversation_to_markdown(chat_history_store)
440
+
441
+ # Save to temporary file
442
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
443
+ f.write(markdown_content)
444
+ temp_file = f.name
445
+
446
+ return gr.update(value=temp_file, visible=True)
447
+
448
+ def export_conversation(history):
449
+ """Export conversation to markdown file"""
450
+ if not history:
451
+ return gr.update(visible=False)
452
+
453
+ markdown_content = export_conversation_to_markdown(history)
454
+
455
+ # Save to temporary file
456
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
457
+ f.write(markdown_content)
458
+ temp_file = f.name
459
+
460
+ return gr.update(value=temp_file, visible=True)
461
+
462
+ # Configuration status display
463
+ def get_configuration_status():
464
+ """Generate a configuration status message for display"""
465
+ status_parts = []
466
+
467
+ if API_KEY_VALID:
468
+ status_parts.append("βœ… **API Key:** Configured and valid")
469
+ else:
470
+ status_parts.append("❌ **API Key:** Not configured - Set `OPENROUTER_API_KEY` in Space secrets")
471
+
472
+ status_parts.append(f"πŸ€– **Model:** {MODEL}")
473
+ status_parts.append(f"🌑️ **Temperature:** 0.7")
474
+ status_parts.append(f"πŸ“ **Max Tokens:** 750")
475
+
476
+ # URL Grounding details
477
+ if GROUNDING_URLS:
478
+ status_parts.append(f"πŸ”— **URL Grounding:** {len(GROUNDING_URLS)} URLs configured")
479
+ # Show first few URLs as examples
480
+ for i, url in enumerate(GROUNDING_URLS[:3], 1):
481
+ priority_label = "Primary" if i <= 2 else "Secondary"
482
+ status_parts.append(f" - [{priority_label}] {url}")
483
+ if len(GROUNDING_URLS) > 3:
484
+ status_parts.append(f" - ... and {len(GROUNDING_URLS) - 3} more URLs")
485
+ else:
486
+ status_parts.append("πŸ”— **URL Grounding:** No URLs configured")
487
+
488
+ if ENABLE_DYNAMIC_URLS:
489
+ status_parts.append("πŸ”„ **Dynamic URLs:** Enabled")
490
+ else:
491
+ status_parts.append("πŸ”„ **Dynamic URLs:** Disabled")
492
+
493
+ if ACCESS_CODE is not None:
494
+ status_parts.append("πŸ” **Access Control:** Enabled")
495
+ else:
496
+ status_parts.append("🌐 **Access:** Public Chatbot")
497
+
498
+ # System Prompt (add at the end)
499
+ status_parts.append("") # Empty line for spacing
500
+ status_parts.append("**System Prompt:**")
501
+ status_parts.append(f"{SYSTEM_PROMPT}")
502
+
503
+ return "\n".join(status_parts)
504
+
505
+ # Create interface with access code protection
506
+ with gr.Blocks(title=SPACE_NAME) as demo:
507
+ gr.Markdown(f"# {SPACE_NAME}")
508
+ gr.Markdown(SPACE_DESCRIPTION)
509
+
510
+ # Access code section (shown only if ACCESS_CODE is set)
511
+ with gr.Column(visible=(ACCESS_CODE is not None)) as access_section:
512
+ gr.Markdown("### πŸ” Access Required")
513
+ gr.Markdown("Please enter the access code provided by your instructor:")
514
+
515
+ access_input = gr.Textbox(
516
+ label="Access Code",
517
+ placeholder="Enter access code...",
518
+ type="password"
519
+ )
520
+ access_btn = gr.Button("Submit", variant="primary")
521
+ access_error = gr.Markdown(visible=False)
522
+
523
+ # Main chat interface (hidden until access granted)
524
+ with gr.Column(visible=(ACCESS_CODE is None)) as chat_section:
525
+ chat_interface = gr.ChatInterface(
526
+ fn=store_and_generate_response, # Use wrapper function to store history
527
+ title="", # Title already shown above
528
+ description="", # Description already shown above
529
+ examples=['I have to write a research paper. Where do I start?', 'What lines of inquiry would be most fruitful?'],
530
+ type="messages" # Use modern message format for better compatibility
531
+ )
532
+
533
+ # Export functionality
534
+ with gr.Row():
535
+ export_btn = gr.Button("πŸ“₯ Export Conversation", variant="secondary", size="sm")
536
+ export_file = gr.File(label="Download Conversation", visible=False)
537
+
538
+ # Connect export functionality
539
+ export_btn.click(
540
+ export_current_conversation,
541
+ outputs=[export_file]
542
+ )
543
+
544
+ # Configuration status (always visible)
545
+ with gr.Accordion("πŸ“Š Configuration Status", open=not API_KEY_VALID):
546
+ gr.Markdown(get_configuration_status())
547
+
548
+ # Connect access verification
549
+ if ACCESS_CODE is not None:
550
+ access_btn.click(
551
+ verify_access_code,
552
+ inputs=[access_input],
553
+ outputs=[access_error, chat_section, access_granted]
554
+ )
555
+ access_input.submit(
556
+ verify_access_code,
557
+ inputs=[access_input],
558
+ outputs=[access_error, chat_section, access_granted]
559
+ )
560
+
561
+ if __name__ == "__main__":
562
+ demo.launch()
config.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Writing Process 101: Brainstorming",
3
+ "description": "A place to help students free write and brainstorm.",
4
+ "system_prompt": "You are a pedagogically-minded academic assistant designed for an advanced elective for undergraduate English majors. The class is about the relationship between history and literature, about how to approach historical questions with a literary approach and approach literary questions with an historical approach. Specifically, the class is focusing on the history and dedication of Red Hook in the 1970s, circulating around novels, short stories, and other narratives (not just fictional) related to Red Hook. Your approach follows constructivist learning principles: build on students' prior knowledge, scaffold complex concepts through graduated questioning, and use Socratic dialogue to guide discovery. When students pose a question or raise an idea, respond with questions. Each question 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.\n\nUse a warm tone, encourage engagement, reflect on what the students bring to this process that AI cannot. Develop exercises to help students brainstorm.",
5
+ "model": "anthropic/claude-3.5-haiku",
6
+ "api_key_var": "OPENROUTER_API_KEY",
7
+ "temperature": 0.7,
8
+ "max_tokens": 750,
9
+ "examples": "['I have to write a research paper. Where do I start?', 'What lines of inquiry would be most fruitful?']",
10
+ "grounding_urls": "[]",
11
+ "enable_dynamic_urls": false
12
+ }
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ gradio>=5.38.0
2
+ requests>=2.32.3
3
+ beautifulsoup4>=4.12.3
4
+ python-dotenv>=1.0.0