Update app.py
Browse files
app.py
CHANGED
@@ -33,21 +33,21 @@ def validate_api_key():
|
|
33 |
"""Validate API key configuration with detailed logging"""
|
34 |
if not API_KEY:
|
35 |
print(f"β οΈ API KEY CONFIGURATION ERROR:")
|
36 |
-
print(f" Variable name:
|
37 |
print(f" Status: Not set or empty")
|
38 |
-
print(f" Action needed: Set '
|
39 |
print(f" Expected format: sk-or-xxxxxxxxxx")
|
40 |
return False
|
41 |
elif not API_KEY.startswith('sk-or-'):
|
42 |
print(f"β οΈ API KEY FORMAT WARNING:")
|
43 |
-
print(f" Variable name:
|
44 |
print(f" Current value: {API_KEY[:10]}..." if len(API_KEY) > 10 else API_KEY)
|
45 |
print(f" Expected format: sk-or-xxxxxxxxxx")
|
46 |
print(f" Note: OpenRouter keys should start with 'sk-or-'")
|
47 |
return True # Still try to use it
|
48 |
else:
|
49 |
print(f"β
API Key configured successfully")
|
50 |
-
print(f" Variable:
|
51 |
print(f" Format: Valid OpenRouter key")
|
52 |
return True
|
53 |
|
@@ -63,79 +63,63 @@ def validate_url_domain(url):
|
|
63 |
try:
|
64 |
from urllib.parse import urlparse
|
65 |
parsed = urlparse(url)
|
66 |
-
#
|
67 |
-
|
68 |
-
|
|
|
|
|
|
|
69 |
except:
|
70 |
-
|
71 |
-
return False
|
72 |
|
73 |
def fetch_url_content(url):
|
74 |
-
"""
|
75 |
if not validate_url_domain(url):
|
76 |
-
return f"
|
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
|
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 |
-
#
|
93 |
-
|
94 |
-
element.decompose()
|
95 |
|
96 |
-
#
|
97 |
-
|
98 |
-
|
99 |
|
100 |
-
#
|
|
|
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
|
104 |
|
105 |
-
#
|
106 |
if len(text) > 4000:
|
107 |
-
|
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 |
-
|
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
|
122 |
|
123 |
def extract_urls_from_text(text):
|
124 |
-
"""Extract URLs from text using regex
|
125 |
-
|
126 |
-
url_pattern = r'https?://[^\s<>"{}|\\^`\[\]"]+'
|
127 |
urls = re.findall(url_pattern, text)
|
128 |
|
129 |
-
#
|
130 |
-
|
131 |
for url in urls:
|
132 |
-
#
|
133 |
-
url = url.rstrip('.,!?;:')
|
134 |
-
# Basic domain validation
|
135 |
if '.' in url and len(url) > 10:
|
136 |
-
|
137 |
|
138 |
-
return
|
139 |
|
140 |
# Global cache for URL content to avoid re-crawling in generated spaces
|
141 |
_url_content_cache = {}
|
@@ -174,62 +158,41 @@ def export_conversation_to_markdown(conversation_history):
|
|
174 |
|
175 |
markdown_content = f"""# Conversation Export
|
176 |
Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|
|
177 |
|
178 |
---
|
179 |
|
180 |
"""
|
181 |
|
182 |
-
message_pair_count = 0
|
183 |
for i, message in enumerate(conversation_history):
|
184 |
if isinstance(message, dict):
|
185 |
-
|
186 |
-
|
|
|
187 |
|
188 |
-
if role ==
|
189 |
-
|
190 |
-
|
191 |
-
|
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 |
-
#
|
195 |
-
message_pair_count += 1
|
196 |
user_msg, assistant_msg = message[0], message[1]
|
197 |
if user_msg:
|
198 |
-
markdown_content += f"## User
|
199 |
if assistant_msg:
|
200 |
-
markdown_content += f"## Assistant
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
201 |
|
202 |
return markdown_content
|
203 |
|
204 |
-
#
|
205 |
-
|
206 |
-
try:
|
207 |
-
import faiss
|
208 |
-
import numpy as np
|
209 |
-
import base64
|
210 |
-
|
211 |
-
class SimpleRAGContext:
|
212 |
-
def __init__(self, rag_data):
|
213 |
-
# Deserialize FAISS index
|
214 |
-
index_bytes = base64.b64decode(rag_data['index_base64'])
|
215 |
-
self.index = faiss.deserialize_index(index_bytes)
|
216 |
-
|
217 |
-
# Restore chunks and mappings
|
218 |
-
self.chunks = rag_data['chunks']
|
219 |
-
self.chunk_ids = rag_data['chunk_ids']
|
220 |
-
|
221 |
-
def get_context(self, query, max_chunks=3):
|
222 |
-
"""Get relevant context - simplified version"""
|
223 |
-
# In production, you'd compute query embedding here
|
224 |
-
# For now, return a simple message
|
225 |
-
return "\n\n[RAG context would be retrieved here based on similarity search]\n\n"
|
226 |
-
|
227 |
-
rag_context_provider = SimpleRAGContext(RAG_DATA)
|
228 |
-
except Exception as e:
|
229 |
-
print(f"Failed to initialize RAG: {e}")
|
230 |
-
rag_context_provider = None
|
231 |
-
else:
|
232 |
-
rag_context_provider = None
|
233 |
|
234 |
def generate_response(message, history):
|
235 |
"""Generate response using OpenRouter API"""
|
@@ -240,10 +203,10 @@ def generate_response(message, history):
|
|
240 |
error_msg += f"Please configure your OpenRouter API key:\n"
|
241 |
error_msg += f"1. Go to Settings (βοΈ) in your HuggingFace Space\n"
|
242 |
error_msg += f"2. Click 'Variables and secrets'\n"
|
243 |
-
error_msg += f"3. Add secret: **
|
244 |
error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\n\n"
|
245 |
error_msg += f"Get your API key at: https://openrouter.ai/keys"
|
246 |
-
print(f"β API request failed: No API key configured for
|
247 |
return error_msg
|
248 |
|
249 |
# Get grounding context
|
@@ -259,9 +222,8 @@ def generate_response(message, history):
|
|
259 |
if ENABLE_DYNAMIC_URLS:
|
260 |
urls_in_message = extract_urls_from_text(message)
|
261 |
if urls_in_message:
|
262 |
-
# Fetch content from URLs mentioned in the message
|
263 |
dynamic_context_parts = []
|
264 |
-
for url in urls_in_message[:
|
265 |
content = fetch_url_content(url)
|
266 |
dynamic_context_parts.append(f"\n\nDynamic context from {url}:\n{content}")
|
267 |
if dynamic_context_parts:
|
@@ -270,16 +232,19 @@ def generate_response(message, history):
|
|
270 |
# Build enhanced system prompt with grounding context
|
271 |
enhanced_system_prompt = SYSTEM_PROMPT + grounding_context
|
272 |
|
273 |
-
# Build
|
274 |
-
messages = [{
|
|
|
|
|
|
|
275 |
|
276 |
-
# Add conversation history -
|
277 |
for chat in history:
|
278 |
if isinstance(chat, dict):
|
279 |
-
#
|
280 |
messages.append(chat)
|
281 |
elif isinstance(chat, (list, tuple)) and len(chat) >= 2:
|
282 |
-
# Legacy format:
|
283 |
user_msg, assistant_msg = chat[0], chat[1]
|
284 |
if user_msg:
|
285 |
messages.append({"role": "user", "content": user_msg})
|
@@ -289,12 +254,8 @@ def generate_response(message, history):
|
|
289 |
# Add current message
|
290 |
messages.append({"role": "user", "content": message})
|
291 |
|
292 |
-
# Make API request with enhanced error handling
|
293 |
try:
|
294 |
-
|
295 |
-
print(f" Model: {MODEL}")
|
296 |
-
print(f" Messages: {len(messages)} in conversation")
|
297 |
-
|
298 |
response = requests.post(
|
299 |
url="https://openrouter.ai/api/v1/chat/completions",
|
300 |
headers={
|
@@ -307,38 +268,18 @@ def generate_response(message, history):
|
|
307 |
"model": MODEL,
|
308 |
"messages": messages,
|
309 |
"temperature": 0.7,
|
310 |
-
"max_tokens": 1500
|
|
|
311 |
},
|
312 |
timeout=30
|
313 |
)
|
314 |
|
315 |
-
print(f"π‘ API Response: {response.status_code}")
|
316 |
-
|
317 |
if response.status_code == 200:
|
318 |
try:
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
print(f"β οΈ API response missing choices: {result}")
|
324 |
-
return "API Error: No response choices available"
|
325 |
-
elif 'message' not in result['choices'][0]:
|
326 |
-
print(f"β οΈ API response missing message: {result}")
|
327 |
-
return "API Error: No message in response"
|
328 |
-
elif 'content' not in result['choices'][0]['message']:
|
329 |
-
print(f"β οΈ API response missing content: {result}")
|
330 |
-
return "API Error: No content in message"
|
331 |
-
else:
|
332 |
-
content = result['choices'][0]['message']['content']
|
333 |
-
|
334 |
-
# Check for empty content
|
335 |
-
if not content or content.strip() == "":
|
336 |
-
print(f"β οΈ API returned empty content")
|
337 |
-
return "API Error: Empty response content"
|
338 |
-
|
339 |
-
print(f"β
API request successful")
|
340 |
-
return content
|
341 |
-
|
342 |
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
343 |
print(f"β Failed to parse API response: {str(e)}")
|
344 |
return f"API Error: Failed to parse response - {str(e)}"
|
@@ -346,7 +287,7 @@ def generate_response(message, history):
|
|
346 |
error_msg = f"π **Authentication Error**\n\n"
|
347 |
error_msg += f"Your API key appears to be invalid or expired.\n\n"
|
348 |
error_msg += f"**Troubleshooting:**\n"
|
349 |
-
error_msg += f"1. Check that your **
|
350 |
error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\n"
|
351 |
error_msg += f"3. Ensure your key starts with `sk-or-`\n"
|
352 |
error_msg += f"4. Check that you have credits on your OpenRouter account"
|
@@ -355,104 +296,64 @@ def generate_response(message, history):
|
|
355 |
elif response.status_code == 429:
|
356 |
error_msg = f"β±οΈ **Rate Limit Exceeded**\n\n"
|
357 |
error_msg += f"Too many requests. Please wait a moment and try again.\n\n"
|
358 |
-
error_msg += f"
|
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,
|
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
|
392 |
-
|
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"
|
401 |
-
|
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"
|
409 |
-
error_msg += f"An unexpected error occurred
|
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 |
-
#
|
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 |
-
|
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),
|
429 |
else:
|
430 |
-
|
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 |
-
"""
|
435 |
-
|
436 |
if ACCESS_CODE and not _access_granted_global:
|
437 |
-
return "Please enter the access code
|
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
|
445 |
global chat_history_store
|
446 |
|
447 |
-
#
|
448 |
-
chat_history_store = history.copy() if history else []
|
449 |
-
|
450 |
-
# Generate response using the protected function
|
451 |
response = protected_generate_response(message, history)
|
452 |
|
453 |
-
#
|
454 |
-
chat_history_store
|
455 |
-
chat_history_store.append({"role": "assistant", "content": response})
|
456 |
|
457 |
return response
|
458 |
|
@@ -464,35 +365,27 @@ def export_current_conversation():
|
|
464 |
markdown_content = export_conversation_to_markdown(chat_history_store)
|
465 |
|
466 |
# Save to temporary file
|
467 |
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False
|
468 |
f.write(markdown_content)
|
469 |
temp_file = f.name
|
470 |
|
471 |
return gr.update(value=temp_file, visible=True)
|
472 |
|
473 |
def export_conversation(history):
|
474 |
-
"""Export conversation
|
475 |
if not history:
|
476 |
-
return
|
477 |
-
|
478 |
-
markdown_content = export_conversation_to_markdown(history)
|
479 |
-
|
480 |
-
# Save to temporary file
|
481 |
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False, encoding='utf-8') as f:
|
482 |
-
f.write(markdown_content)
|
483 |
-
temp_file = f.name
|
484 |
|
485 |
-
return
|
486 |
|
487 |
-
# Configuration status display
|
488 |
def get_configuration_status():
|
489 |
-
"""
|
490 |
status_parts = []
|
491 |
|
492 |
if API_KEY_VALID:
|
493 |
status_parts.append("β
**API Key:** Configured and valid")
|
494 |
else:
|
495 |
-
status_parts.append("β **API Key:** Not configured - Set `
|
496 |
|
497 |
status_parts.append(f"π€ **Model:** {MODEL}")
|
498 |
status_parts.append(f"π‘οΈ **Temperature:** 0.7")
|
@@ -514,6 +407,9 @@ def get_configuration_status():
|
|
514 |
|
515 |
return "\n".join(status_parts)
|
516 |
|
|
|
|
|
|
|
517 |
# Create interface with access code protection
|
518 |
with gr.Blocks(title=SPACE_NAME) as demo:
|
519 |
gr.Markdown(f"# {SPACE_NAME}")
|
@@ -523,10 +419,11 @@ with gr.Blocks(title=SPACE_NAME) as demo:
|
|
523 |
with gr.Accordion("π Configuration Status", open=not API_KEY_VALID):
|
524 |
gr.Markdown(get_configuration_status())
|
525 |
|
526 |
-
# Access code section (
|
527 |
-
|
528 |
-
gr.
|
529 |
-
|
|
|
530 |
|
531 |
access_input = gr.Textbox(
|
532 |
label="Access Code",
|
@@ -571,4 +468,4 @@ with gr.Blocks(title=SPACE_NAME) as demo:
|
|
571 |
)
|
572 |
|
573 |
if __name__ == "__main__":
|
574 |
-
demo.launch()
|
|
|
33 |
"""Validate API key configuration with detailed logging"""
|
34 |
if not API_KEY:
|
35 |
print(f"β οΈ API KEY CONFIGURATION ERROR:")
|
36 |
+
print(f" Variable name: OPENROUTER_API_KEY")
|
37 |
print(f" Status: Not set or empty")
|
38 |
+
print(f" Action needed: Set 'OPENROUTER_API_KEY' in HuggingFace Space secrets")
|
39 |
print(f" Expected format: sk-or-xxxxxxxxxx")
|
40 |
return False
|
41 |
elif not API_KEY.startswith('sk-or-'):
|
42 |
print(f"β οΈ API KEY FORMAT WARNING:")
|
43 |
+
print(f" Variable name: OPENROUTER_API_KEY")
|
44 |
print(f" Current value: {API_KEY[:10]}..." if len(API_KEY) > 10 else API_KEY)
|
45 |
print(f" Expected format: sk-or-xxxxxxxxxx")
|
46 |
print(f" Note: OpenRouter keys should start with 'sk-or-'")
|
47 |
return True # Still try to use it
|
48 |
else:
|
49 |
print(f"β
API Key configured successfully")
|
50 |
+
print(f" Variable: OPENROUTER_API_KEY")
|
51 |
print(f" Format: Valid OpenRouter key")
|
52 |
return True
|
53 |
|
|
|
63 |
try:
|
64 |
from urllib.parse import urlparse
|
65 |
parsed = urlparse(url)
|
66 |
+
# Allow common domains and exclude potentially dangerous ones
|
67 |
+
allowed_patterns = [
|
68 |
+
'wikipedia.org', 'britannica.com', 'edu', 'gov', 'arxiv.org',
|
69 |
+
'pubmed.ncbi.nlm.nih.gov', 'scholar.google.com', 'researchgate.net'
|
70 |
+
]
|
71 |
+
return any(pattern in parsed.netloc.lower() for pattern in allowed_patterns)
|
72 |
except:
|
73 |
+
return False
|
|
|
74 |
|
75 |
def fetch_url_content(url):
|
76 |
+
"""Fetch content from URL with enhanced error handling"""
|
77 |
if not validate_url_domain(url):
|
78 |
+
return f"URL domain not in allowed list for security: {url}"
|
79 |
|
80 |
try:
|
|
|
81 |
headers = {
|
82 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
|
|
|
|
|
|
|
83 |
}
|
84 |
+
response = requests.get(url, headers=headers, timeout=10)
|
|
|
85 |
response.raise_for_status()
|
|
|
86 |
|
87 |
+
# Parse HTML and extract text
|
88 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
|
|
89 |
|
90 |
+
# Remove script and style elements
|
91 |
+
for script in soup(["script", "style"]):
|
92 |
+
script.decompose()
|
93 |
|
94 |
+
# Get text and clean it up
|
95 |
+
text = soup.get_text()
|
96 |
lines = (line.strip() for line in text.splitlines())
|
97 |
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
98 |
+
text = ' '.join(chunk for chunk in chunks if chunk)
|
99 |
|
100 |
+
# Limit content length
|
101 |
if len(text) > 4000:
|
102 |
+
text = text[:4000] + "..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
|
104 |
+
return text
|
|
|
105 |
except requests.exceptions.RequestException as e:
|
106 |
return f"Error fetching {url}: {str(e)}"
|
107 |
except Exception as e:
|
108 |
+
return f"Error processing {url}: {str(e)}"
|
109 |
|
110 |
def extract_urls_from_text(text):
|
111 |
+
"""Extract URLs from text using regex"""
|
112 |
+
url_pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
|
|
|
113 |
urls = re.findall(url_pattern, text)
|
114 |
|
115 |
+
# Filter out URLs that are clearly not useful
|
116 |
+
filtered_urls = []
|
117 |
for url in urls:
|
118 |
+
# Basic filtering - must contain a dot and be reasonably long
|
|
|
|
|
119 |
if '.' in url and len(url) > 10:
|
120 |
+
filtered_urls.append(url)
|
121 |
|
122 |
+
return filtered_urls
|
123 |
|
124 |
# Global cache for URL content to avoid re-crawling in generated spaces
|
125 |
_url_content_cache = {}
|
|
|
158 |
|
159 |
markdown_content = f"""# Conversation Export
|
160 |
Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
161 |
+
Space: {SPACE_NAME}
|
162 |
|
163 |
---
|
164 |
|
165 |
"""
|
166 |
|
|
|
167 |
for i, message in enumerate(conversation_history):
|
168 |
if isinstance(message, dict):
|
169 |
+
# New format: {"role": "user", "content": "..."}
|
170 |
+
role = message.get("role", "unknown")
|
171 |
+
content = message.get("content", "")
|
172 |
|
173 |
+
if role == "user":
|
174 |
+
markdown_content += f"## π€ User\n\n{content}\n\n"
|
175 |
+
elif role == "assistant":
|
176 |
+
markdown_content += f"## π€ Assistant\n\n{content}\n\n"
|
|
|
177 |
elif isinstance(message, (list, tuple)) and len(message) >= 2:
|
178 |
+
# Legacy format: [user_msg, assistant_msg]
|
|
|
179 |
user_msg, assistant_msg = message[0], message[1]
|
180 |
if user_msg:
|
181 |
+
markdown_content += f"## π€ User\n\n{user_msg}\n\n"
|
182 |
if assistant_msg:
|
183 |
+
markdown_content += f"## π€ Assistant\n\n{assistant_msg}\n\n"
|
184 |
+
|
185 |
+
markdown_content += f"""
|
186 |
+
|
187 |
+
---
|
188 |
+
|
189 |
+
*Exported from {SPACE_NAME} on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
|
190 |
+
"""
|
191 |
|
192 |
return markdown_content
|
193 |
|
194 |
+
# Global variable to store chat history for export
|
195 |
+
chat_history_store = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
196 |
|
197 |
def generate_response(message, history):
|
198 |
"""Generate response using OpenRouter API"""
|
|
|
203 |
error_msg += f"Please configure your OpenRouter API key:\n"
|
204 |
error_msg += f"1. Go to Settings (βοΈ) in your HuggingFace Space\n"
|
205 |
error_msg += f"2. Click 'Variables and secrets'\n"
|
206 |
+
error_msg += f"3. Add secret: **OPENROUTER_API_KEY**\n"
|
207 |
error_msg += f"4. Value: Your OpenRouter API key (starts with `sk-or-`)\n\n"
|
208 |
error_msg += f"Get your API key at: https://openrouter.ai/keys"
|
209 |
+
print(f"β API request failed: No API key configured for OPENROUTER_API_KEY")
|
210 |
return error_msg
|
211 |
|
212 |
# Get grounding context
|
|
|
222 |
if ENABLE_DYNAMIC_URLS:
|
223 |
urls_in_message = extract_urls_from_text(message)
|
224 |
if urls_in_message:
|
|
|
225 |
dynamic_context_parts = []
|
226 |
+
for url in urls_in_message[:2]: # Limit to 2 URLs to avoid overwhelming context
|
227 |
content = fetch_url_content(url)
|
228 |
dynamic_context_parts.append(f"\n\nDynamic context from {url}:\n{content}")
|
229 |
if dynamic_context_parts:
|
|
|
232 |
# Build enhanced system prompt with grounding context
|
233 |
enhanced_system_prompt = SYSTEM_PROMPT + grounding_context
|
234 |
|
235 |
+
# Build conversation history for API
|
236 |
+
messages = [{
|
237 |
+
"role": "system",
|
238 |
+
"content": enhanced_system_prompt
|
239 |
+
}]
|
240 |
|
241 |
+
# Add conversation history - Support both new messages format and legacy tuple format
|
242 |
for chat in history:
|
243 |
if isinstance(chat, dict):
|
244 |
+
# New format: {"role": "user", "content": "..."}
|
245 |
messages.append(chat)
|
246 |
elif isinstance(chat, (list, tuple)) and len(chat) >= 2:
|
247 |
+
# Legacy format: ("user msg", "bot msg")
|
248 |
user_msg, assistant_msg = chat[0], chat[1]
|
249 |
if user_msg:
|
250 |
messages.append({"role": "user", "content": user_msg})
|
|
|
254 |
# Add current message
|
255 |
messages.append({"role": "user", "content": message})
|
256 |
|
|
|
257 |
try:
|
258 |
+
# Make API request to OpenRouter
|
|
|
|
|
|
|
259 |
response = requests.post(
|
260 |
url="https://openrouter.ai/api/v1/chat/completions",
|
261 |
headers={
|
|
|
268 |
"model": MODEL,
|
269 |
"messages": messages,
|
270 |
"temperature": 0.7,
|
271 |
+
"max_tokens": 1500,
|
272 |
+
"stream": False
|
273 |
},
|
274 |
timeout=30
|
275 |
)
|
276 |
|
|
|
|
|
277 |
if response.status_code == 200:
|
278 |
try:
|
279 |
+
response_data = response.json()
|
280 |
+
assistant_response = response_data['choices'][0]['message']['content']
|
281 |
+
print(f"β
API request successful")
|
282 |
+
return assistant_response
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
283 |
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
284 |
print(f"β Failed to parse API response: {str(e)}")
|
285 |
return f"API Error: Failed to parse response - {str(e)}"
|
|
|
287 |
error_msg = f"π **Authentication Error**\n\n"
|
288 |
error_msg += f"Your API key appears to be invalid or expired.\n\n"
|
289 |
error_msg += f"**Troubleshooting:**\n"
|
290 |
+
error_msg += f"1. Check that your **OPENROUTER_API_KEY** secret is set correctly\n"
|
291 |
error_msg += f"2. Verify your API key at: https://openrouter.ai/keys\n"
|
292 |
error_msg += f"3. Ensure your key starts with `sk-or-`\n"
|
293 |
error_msg += f"4. Check that you have credits on your OpenRouter account"
|
|
|
296 |
elif response.status_code == 429:
|
297 |
error_msg = f"β±οΈ **Rate Limit Exceeded**\n\n"
|
298 |
error_msg += f"Too many requests. Please wait a moment and try again.\n\n"
|
299 |
+
error_msg += f"If this persists, check your OpenRouter plan limits."
|
|
|
|
|
|
|
300 |
print(f"β Rate limit exceeded: {response.status_code}")
|
301 |
return error_msg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
302 |
else:
|
303 |
error_msg = f"π« **API Error {response.status_code}**\n\n"
|
304 |
error_msg += f"An unexpected error occurred. Please try again.\n\n"
|
305 |
+
error_msg += f"If this persists, the issue may be with the OpenRouter service."
|
|
|
|
|
|
|
306 |
print(f"β API error: {response.status_code} - {response.text[:200]}")
|
307 |
return error_msg
|
308 |
|
309 |
except requests.exceptions.Timeout:
|
310 |
error_msg = f"β° **Request Timeout**\n\n"
|
311 |
+
error_msg += f"The request took too long to complete. Please try again with a shorter message."
|
312 |
+
print(f"β Request timeout")
|
|
|
|
|
|
|
|
|
313 |
return error_msg
|
314 |
except requests.exceptions.ConnectionError:
|
315 |
error_msg = f"π **Connection Error**\n\n"
|
316 |
+
error_msg += f"Unable to connect to the AI service. Please check your internet connection and try again."
|
317 |
+
print(f"β Connection error")
|
|
|
|
|
|
|
|
|
318 |
return error_msg
|
319 |
except Exception as e:
|
320 |
+
error_msg = f"π₯ **Unexpected Error**\n\n"
|
321 |
+
error_msg += f"An unexpected error occurred: {str(e)}\n\n"
|
|
|
322 |
error_msg += f"Please try again or contact support if this persists."
|
323 |
print(f"β Unexpected error: {str(e)}")
|
324 |
return error_msg
|
325 |
|
326 |
+
# Global state for access control
|
327 |
access_granted = gr.State(False)
|
|
|
328 |
|
329 |
def verify_access_code(code):
|
330 |
"""Verify the access code"""
|
331 |
global _access_granted_global
|
332 |
if not ACCESS_CODE:
|
333 |
+
return gr.update(visible=False), gr.update(visible=True), True
|
|
|
334 |
|
335 |
+
if code.strip() == ACCESS_CODE.strip():
|
336 |
_access_granted_global = True
|
337 |
+
return gr.update(visible=False), gr.update(visible=True), True
|
338 |
else:
|
339 |
+
return gr.update(value="β Incorrect access code. Please try again.", visible=True), gr.update(visible=False), False
|
|
|
340 |
|
341 |
def protected_generate_response(message, history):
|
342 |
+
"""Wrapper that checks access before generating response"""
|
343 |
+
global _access_granted_global
|
344 |
if ACCESS_CODE and not _access_granted_global:
|
345 |
+
return "Access denied. Please enter the correct access code."
|
346 |
return generate_response(message, history)
|
347 |
|
|
|
|
|
|
|
348 |
def store_and_generate_response(message, history):
|
349 |
+
"""Wrapper that stores conversation and generates response"""
|
350 |
global chat_history_store
|
351 |
|
352 |
+
# Generate response
|
|
|
|
|
|
|
353 |
response = protected_generate_response(message, history)
|
354 |
|
355 |
+
# Store in global history for export (using new format)
|
356 |
+
chat_history_store = history + [{"role": "user", "content": message}, {"role": "assistant", "content": response}]
|
|
|
357 |
|
358 |
return response
|
359 |
|
|
|
365 |
markdown_content = export_conversation_to_markdown(chat_history_store)
|
366 |
|
367 |
# Save to temporary file
|
368 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
369 |
f.write(markdown_content)
|
370 |
temp_file = f.name
|
371 |
|
372 |
return gr.update(value=temp_file, visible=True)
|
373 |
|
374 |
def export_conversation(history):
|
375 |
+
"""Export conversation from gradio history"""
|
376 |
if not history:
|
377 |
+
return "No conversation to export."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
378 |
|
379 |
+
return export_conversation_to_markdown(history)
|
380 |
|
|
|
381 |
def get_configuration_status():
|
382 |
+
"""Get current configuration status for display"""
|
383 |
status_parts = []
|
384 |
|
385 |
if API_KEY_VALID:
|
386 |
status_parts.append("β
**API Key:** Configured and valid")
|
387 |
else:
|
388 |
+
status_parts.append("β **API Key:** Not configured - Set `OPENROUTER_API_KEY` in Space secrets")
|
389 |
|
390 |
status_parts.append(f"π€ **Model:** {MODEL}")
|
391 |
status_parts.append(f"π‘οΈ **Temperature:** 0.7")
|
|
|
407 |
|
408 |
return "\n".join(status_parts)
|
409 |
|
410 |
+
# Global access tracking
|
411 |
+
_access_granted_global = not bool(ACCESS_CODE) # If no access code, grant access
|
412 |
+
|
413 |
# Create interface with access code protection
|
414 |
with gr.Blocks(title=SPACE_NAME) as demo:
|
415 |
gr.Markdown(f"# {SPACE_NAME}")
|
|
|
419 |
with gr.Accordion("π Configuration Status", open=not API_KEY_VALID):
|
420 |
gr.Markdown(get_configuration_status())
|
421 |
|
422 |
+
# Access code section (only visible if ACCESS_CODE is set)
|
423 |
+
if ACCESS_CODE:
|
424 |
+
with gr.Column(visible=True) as access_section:
|
425 |
+
gr.Markdown("π **Access Required**")
|
426 |
+
gr.Markdown("Please enter the access code to use this space.")
|
427 |
|
428 |
access_input = gr.Textbox(
|
429 |
label="Access Code",
|
|
|
468 |
)
|
469 |
|
470 |
if __name__ == "__main__":
|
471 |
+
demo.launch()
|