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