Spaces:
Running
Running
fix: remove unsupported clear_btn parameter from ChatInterface
Browse files
app.py
CHANGED
@@ -19,12 +19,12 @@ SPACE_DESCRIPTION = 'Interactive STEM adventure game guide'
|
|
19 |
DEFAULT_CONFIG = {
|
20 |
'name': SPACE_NAME,
|
21 |
'description': SPACE_DESCRIPTION,
|
22 |
-
'system_prompt':
|
23 |
'temperature': 0.6,
|
24 |
'max_tokens': 1000,
|
25 |
'model': 'google/gemini-2.0-flash-001',
|
26 |
'api_key_var': 'API_KEY',
|
27 |
-
'theme': '
|
28 |
'grounding_urls': ["https://en.wikipedia.org/wiki/List_of_experiments", "https://en.wikipedia.org/wiki/Scientific_method", "https://en.wikipedia.org/wiki/List_of_experiments#Biology", "https://en.wikipedia.org/wiki/List_of_experiments#Astronomy", "https://en.wikipedia.org/wiki/List_of_experiments#Chemistry", "https://en.wikipedia.org/wiki/List_of_experiments#Physics", "https://en.wikipedia.org/wiki/List_of_experiments#Geology"],
|
29 |
'enable_dynamic_urls': True,
|
30 |
'enable_file_upload': True,
|
@@ -92,49 +92,56 @@ class ConfigurationManager:
|
|
92 |
backup_path = os.path.join(self.backup_dir, f"config_{timestamp}.json")
|
93 |
|
94 |
with open(self.config_path, 'r') as source:
|
95 |
-
|
|
|
|
|
96 |
|
97 |
-
|
98 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
|
100 |
-
|
101 |
-
|
102 |
-
if len(backups) > 10:
|
103 |
-
for old_backup in backups[:-10]:
|
104 |
os.remove(os.path.join(self.backup_dir, old_backup))
|
105 |
except Exception as e:
|
106 |
-
print(f"
|
107 |
-
|
108 |
-
def get(self, key: str, default: Any = None) -> Any:
|
109 |
-
"""Get configuration value"""
|
110 |
-
if self._config is None:
|
111 |
-
self.load()
|
112 |
-
return self._config.get(key, default)
|
113 |
|
114 |
|
115 |
-
#
|
116 |
config_manager = ConfigurationManager()
|
117 |
config = config_manager.load()
|
118 |
|
119 |
-
#
|
120 |
SPACE_NAME = config.get('name', DEFAULT_CONFIG['name'])
|
121 |
SPACE_DESCRIPTION = config.get('description', DEFAULT_CONFIG['description'])
|
122 |
SYSTEM_PROMPT = config.get('system_prompt', DEFAULT_CONFIG['system_prompt'])
|
123 |
-
temperature = config.get('temperature', DEFAULT_CONFIG['temperature'])
|
124 |
-
max_tokens = config.get('max_tokens', DEFAULT_CONFIG['max_tokens'])
|
125 |
MODEL = config.get('model', DEFAULT_CONFIG['model'])
|
|
|
126 |
THEME = config.get('theme', DEFAULT_CONFIG['theme'])
|
127 |
GROUNDING_URLS = config.get('grounding_urls', DEFAULT_CONFIG['grounding_urls'])
|
128 |
ENABLE_DYNAMIC_URLS = config.get('enable_dynamic_urls', DEFAULT_CONFIG['enable_dynamic_urls'])
|
129 |
-
ENABLE_FILE_UPLOAD = config.get('enable_file_upload', DEFAULT_CONFIG
|
|
|
|
|
|
|
|
|
130 |
|
131 |
-
#
|
132 |
-
|
133 |
-
|
134 |
-
|
|
|
|
|
135 |
|
136 |
|
137 |
-
# Utility functions
|
138 |
def validate_api_key() -> bool:
|
139 |
"""Validate API key configuration"""
|
140 |
if not API_KEY:
|
@@ -158,104 +165,100 @@ def validate_url_domain(url: str) -> bool:
|
|
158 |
try:
|
159 |
from urllib.parse import urlparse
|
160 |
parsed = urlparse(url)
|
161 |
-
return bool(parsed.netloc and
|
162 |
except:
|
163 |
return False
|
164 |
|
165 |
|
166 |
-
def fetch_url_content(url: str
|
167 |
-
"""Fetch and
|
168 |
-
if not validate_url_domain(url):
|
169 |
-
return f"Invalid URL format: {url}"
|
170 |
-
|
171 |
try:
|
|
|
|
|
|
|
172 |
headers = {
|
173 |
-
'User-Agent': 'Mozilla/5.0 (
|
174 |
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
175 |
-
'Accept-Language': 'en-US,en;q=0.5',
|
176 |
-
'Accept-Encoding': 'gzip, deflate',
|
177 |
-
'Connection': 'keep-alive'
|
178 |
}
|
179 |
|
180 |
-
response = requests.get(url,
|
181 |
response.raise_for_status()
|
182 |
-
soup = BeautifulSoup(response.content, 'html.parser')
|
183 |
-
|
184 |
-
# Remove non-content elements
|
185 |
-
for element in soup(["script", "style", "nav", "header", "footer", "aside", "form"]):
|
186 |
-
element.decompose()
|
187 |
-
|
188 |
-
# Extract main content
|
189 |
-
main_content = (
|
190 |
-
soup.find('main') or
|
191 |
-
soup.find('article') or
|
192 |
-
soup.find('div', class_=lambda x: bool(x and 'content' in x.lower())) or
|
193 |
-
soup
|
194 |
-
)
|
195 |
-
text = main_content.get_text()
|
196 |
|
197 |
-
|
198 |
-
lines = (line.strip() for line in text.splitlines())
|
199 |
-
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
200 |
-
text = ' '.join(chunk for chunk in chunks if chunk and len(chunk) > 2)
|
201 |
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
206 |
-
|
207 |
-
|
208 |
-
|
209 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
210 |
|
211 |
-
|
|
|
|
|
|
|
|
|
212 |
|
|
|
|
|
|
|
213 |
except requests.exceptions.Timeout:
|
214 |
-
return f"Timeout
|
215 |
except requests.exceptions.RequestException as e:
|
216 |
-
return f"Error
|
217 |
except Exception as e:
|
218 |
-
return f"
|
219 |
|
220 |
|
221 |
def extract_urls_from_text(text: str) -> List[str]:
|
222 |
-
"""Extract
|
223 |
-
|
224 |
-
return []
|
225 |
-
|
226 |
-
url_pattern = r'https?://[^\s<>"{}|\\^`\[\]"]+'
|
227 |
urls = re.findall(url_pattern, text)
|
228 |
-
|
229 |
-
validated_urls = []
|
230 |
-
for url in urls:
|
231 |
-
url = url.rstrip('.,!?;:')
|
232 |
-
if validate_url_domain(url) and len(url) > 10:
|
233 |
-
validated_urls.append(url)
|
234 |
-
|
235 |
-
return validated_urls
|
236 |
|
237 |
|
238 |
-
def
|
239 |
-
"""Process uploaded file
|
240 |
-
if not file_path or not os.path.exists(file_path):
|
241 |
-
return "β File not found"
|
242 |
-
|
243 |
try:
|
|
|
|
|
|
|
244 |
file_size = os.path.getsize(file_path)
|
245 |
file_name = os.path.basename(file_path)
|
246 |
-
_, ext = os.path.splitext(
|
|
|
247 |
|
248 |
-
# Text
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
256 |
|
257 |
-
|
258 |
-
|
|
|
|
|
259 |
content = f.read(max_chars)
|
260 |
if len(content) == max_chars:
|
261 |
content += "\n... [truncated]"
|
@@ -288,31 +291,26 @@ def get_grounding_context() -> str:
|
|
288 |
try:
|
289 |
urls = json.loads(urls)
|
290 |
except:
|
291 |
-
|
292 |
|
293 |
if not urls:
|
294 |
return ""
|
295 |
|
296 |
-
|
297 |
-
cache_key = tuple(sorted([url for url in urls if url and url.strip()]))
|
298 |
-
if cache_key in _url_content_cache:
|
299 |
-
return _url_content_cache[cache_key]
|
300 |
|
301 |
-
#
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
content = fetch_url_content(url
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
if context_parts:
|
311 |
-
result = "\n\n" + "\n\n".join(context_parts) + "\n\n"
|
312 |
|
313 |
-
|
314 |
-
|
315 |
-
return
|
316 |
|
317 |
|
318 |
def export_conversation_to_markdown(history: List[Dict[str, str]]) -> str:
|
@@ -330,23 +328,21 @@ Model: {MODEL}
|
|
330 |
"""
|
331 |
|
332 |
message_count = 0
|
333 |
-
for
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
markdown_content += f"## Assistant Response {message_count}\n\n{content}\n\n---\n\n"
|
343 |
|
344 |
return markdown_content
|
345 |
|
346 |
|
347 |
-
def generate_response(message: str, history: List[Dict[str, str]], files: Optional[List] = None) -> str:
|
348 |
-
"""Generate response
|
349 |
-
|
350 |
# API key validation
|
351 |
if not API_KEY:
|
352 |
return f"""π **API Key Required**
|
@@ -367,15 +363,15 @@ Get your API key at: https://openrouter.ai/keys"""
|
|
367 |
file_contents = []
|
368 |
file_names = []
|
369 |
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
|
|
|
|
375 |
try:
|
376 |
-
|
377 |
-
file_path = file_obj.name if hasattr(file_obj, 'name') else str(file_obj)
|
378 |
-
content = process_uploaded_file(file_path)
|
379 |
file_contents.append(content)
|
380 |
file_names.append(os.path.basename(file_path))
|
381 |
print(f"π Processed file: {os.path.basename(file_path)}")
|
@@ -393,149 +389,127 @@ Get your API key at: https://openrouter.ai/keys"""
|
|
393 |
if ENABLE_DYNAMIC_URLS:
|
394 |
urls_in_message = extract_urls_from_text(message)
|
395 |
if urls_in_message:
|
|
|
|
|
396 |
for url in urls_in_message[:3]: # Limit to 3 URLs
|
397 |
content = fetch_url_content(url)
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
enhanced_system_prompt = SYSTEM_PROMPT + grounding_context + file_context
|
402 |
|
403 |
# Build messages
|
404 |
-
messages = [{"role": "system", "content":
|
405 |
-
|
406 |
-
# Add history
|
407 |
-
for
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
413 |
|
414 |
-
# Add current message
|
415 |
-
user_message = message + file_notification
|
416 |
-
messages.append({"role": "user", "content": user_message})
|
417 |
-
|
418 |
-
# Make API request
|
419 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
420 |
response = requests.post(
|
421 |
-
|
422 |
-
headers=
|
423 |
-
|
424 |
-
"Content-Type": "application/json",
|
425 |
-
"HTTP-Referer": "https://huggingface.co",
|
426 |
-
"X-Title": "HuggingFace Space"
|
427 |
-
},
|
428 |
-
json={
|
429 |
-
"model": MODEL,
|
430 |
-
"messages": messages,
|
431 |
-
"temperature": temperature,
|
432 |
-
"max_tokens": max_tokens
|
433 |
-
},
|
434 |
timeout=30
|
435 |
)
|
436 |
|
437 |
if response.status_code == 200:
|
438 |
result = response.json()
|
439 |
-
|
440 |
-
|
|
|
|
|
|
|
|
|
|
|
441 |
else:
|
442 |
-
|
|
|
|
|
443 |
|
444 |
except requests.exceptions.Timeout:
|
445 |
-
return "
|
446 |
-
except requests.exceptions.ConnectionError:
|
447 |
-
return "π Connection error. Check your internet connection and try again."
|
448 |
except Exception as e:
|
449 |
return f"β Error: {str(e)}"
|
450 |
|
451 |
|
452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
453 |
class AccessControl:
|
454 |
-
"""
|
455 |
def __init__(self):
|
456 |
-
self.
|
457 |
|
458 |
-
def verify(self, code: str) ->
|
459 |
"""Verify access code"""
|
460 |
-
if ACCESS_CODE
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
if code == ACCESS_CODE:
|
465 |
-
self.granted = True
|
466 |
-
return True, "β
Access granted!"
|
467 |
-
else:
|
468 |
-
self.granted = False
|
469 |
-
return False, "β Invalid access code. Please try again."
|
470 |
|
471 |
def is_granted(self) -> bool:
|
472 |
"""Check if access is granted"""
|
473 |
-
return self.
|
474 |
|
475 |
|
476 |
-
# Initialize access control
|
477 |
access_control = AccessControl()
|
478 |
|
479 |
|
480 |
-
def
|
481 |
-
"""
|
482 |
-
|
483 |
-
return "Please enter the access code to continue."
|
484 |
-
return generate_response(message, history, files)
|
485 |
-
|
486 |
-
|
487 |
-
# Chat history for export
|
488 |
-
chat_history_store = []
|
489 |
-
|
490 |
-
|
491 |
-
def store_and_generate_response(message: str, history: List[Dict[str, str]], files: Optional[List] = None) -> str:
|
492 |
-
"""Generate response and store history for export"""
|
493 |
-
global chat_history_store
|
494 |
-
|
495 |
-
# Generate response
|
496 |
-
response = protected_generate_response(message, history, files)
|
497 |
-
|
498 |
-
# Update stored history
|
499 |
-
chat_history_store = []
|
500 |
-
for exchange in history:
|
501 |
-
if isinstance(exchange, dict):
|
502 |
-
chat_history_store.append(exchange)
|
503 |
-
elif isinstance(exchange, (list, tuple)) and len(exchange) >= 2:
|
504 |
-
chat_history_store.append({"role": "user", "content": exchange[0]})
|
505 |
-
chat_history_store.append({"role": "assistant", "content": exchange[1]})
|
506 |
-
|
507 |
-
# Add current exchange
|
508 |
-
chat_history_store.append({"role": "user", "content": message})
|
509 |
-
chat_history_store.append({"role": "assistant", "content": response})
|
510 |
-
|
511 |
-
return response
|
512 |
-
|
513 |
-
|
514 |
-
def verify_hf_token_access() -> Tuple[bool, str]:
|
515 |
-
"""Verify HuggingFace token access"""
|
516 |
-
hf_token = os.environ.get("HF_TOKEN")
|
517 |
-
space_id = os.environ.get("SPACE_ID")
|
518 |
-
|
519 |
-
if not hf_token or not space_id:
|
520 |
-
return False, "Missing HF_TOKEN or SPACE_ID"
|
521 |
-
|
522 |
-
try:
|
523 |
-
from huggingface_hub import HfApi
|
524 |
-
api = HfApi(token=hf_token)
|
525 |
-
api.space_info(space_id)
|
526 |
-
return True, "Authenticated successfully"
|
527 |
-
except Exception as e:
|
528 |
-
return False, f"Authentication failed: {str(e)}"
|
529 |
-
|
530 |
-
|
531 |
-
# Create main interface using modern Gradio patterns
|
532 |
-
def create_interface():
|
533 |
-
"""Create the Gradio interface with modern patterns"""
|
534 |
-
|
535 |
-
# Get theme
|
536 |
-
theme = AVAILABLE_THEMES.get(THEME, gr.themes.Default())
|
537 |
-
|
538 |
-
# Validate API key on startup
|
539 |
API_KEY_VALID = validate_api_key()
|
540 |
|
541 |
# Check HuggingFace access
|
@@ -555,40 +529,39 @@ def create_interface():
|
|
555 |
if ACCESS_CODE and not access_control.is_granted():
|
556 |
# Show access code input
|
557 |
gr.Markdown("### π Access Required")
|
558 |
-
gr.Markdown("
|
559 |
|
560 |
with gr.Row():
|
561 |
access_input = gr.Textbox(
|
562 |
label="Access Code",
|
563 |
-
placeholder="Enter access code...",
|
564 |
type="password",
|
|
|
565 |
scale=3
|
566 |
)
|
567 |
access_btn = gr.Button("Submit", variant="primary", scale=1)
|
568 |
|
569 |
-
access_status = gr.Markdown()
|
570 |
|
571 |
-
def
|
572 |
-
|
573 |
-
|
574 |
-
# Force refresh to show chat interface
|
575 |
-
return {access_status: message}, gr.Tabs(selected=0)
|
576 |
else:
|
577 |
-
return
|
578 |
|
579 |
access_btn.click(
|
580 |
-
|
581 |
inputs=[access_input],
|
582 |
-
outputs=[access_status
|
583 |
)
|
584 |
|
585 |
access_input.submit(
|
586 |
-
|
587 |
inputs=[access_input],
|
588 |
-
outputs=[access_status
|
589 |
)
|
590 |
else:
|
591 |
-
# Show chat interface
|
|
|
592 |
examples = config.get('examples', [])
|
593 |
if isinstance(examples, str):
|
594 |
try:
|
@@ -603,17 +576,17 @@ def create_interface():
|
|
603 |
elif examples:
|
604 |
formatted_examples = examples
|
605 |
|
606 |
-
# Create additional inputs
|
607 |
-
additional_inputs =
|
608 |
if ENABLE_FILE_UPLOAD:
|
609 |
-
additional_inputs
|
610 |
gr.File(
|
611 |
label="π Upload Files",
|
612 |
-
file_types=None,
|
613 |
file_count="multiple",
|
|
|
614 |
visible=True
|
615 |
)
|
616 |
-
|
617 |
|
618 |
# Create chat interface
|
619 |
chat_interface = gr.ChatInterface(
|
@@ -624,7 +597,6 @@ def create_interface():
|
|
624 |
type="messages",
|
625 |
additional_inputs=additional_inputs if additional_inputs else None,
|
626 |
submit_btn="Send",
|
627 |
-
clear_btn="Clear",
|
628 |
undo_btn=None,
|
629 |
retry_btn=None
|
630 |
)
|
@@ -637,23 +609,50 @@ def create_interface():
|
|
637 |
size="sm"
|
638 |
)
|
639 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
640 |
# Export handler
|
641 |
-
def prepare_export():
|
642 |
-
|
|
|
643 |
return None
|
644 |
|
645 |
-
content = export_conversation_to_markdown(
|
646 |
-
|
647 |
-
# Create filename
|
648 |
-
space_name_safe = re.sub(r'[^a-zA-Z0-9]+', '_', SPACE_NAME).lower()
|
649 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
650 |
-
filename = f"{
|
651 |
|
652 |
-
|
653 |
-
|
654 |
-
|
655 |
-
|
656 |
-
return str(temp_path)
|
657 |
|
658 |
export_btn.click(
|
659 |
prepare_export,
|
@@ -692,25 +691,43 @@ def create_interface():
|
|
692 |
show_label=True
|
693 |
)
|
694 |
|
695 |
-
#
|
696 |
with gr.Group():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
697 |
edit_system_prompt = gr.Textbox(
|
698 |
label="System Prompt",
|
699 |
value=config.get('system_prompt', ''),
|
700 |
lines=5
|
701 |
)
|
702 |
|
703 |
-
|
704 |
-
label="Model",
|
705 |
-
choices=[
|
706 |
-
"google/gemini-2.0-flash-001",
|
707 |
-
"anthropic/claude-3.5-sonnet",
|
708 |
-
"openai/gpt-4o-mini",
|
709 |
-
"meta-llama/llama-3.1-70b-instruct"
|
710 |
-
],
|
711 |
-
value=config.get('model', MODEL)
|
712 |
-
)
|
713 |
-
|
714 |
with gr.Row():
|
715 |
edit_temperature = gr.Slider(
|
716 |
label="Temperature",
|
@@ -732,88 +749,93 @@ def create_interface():
|
|
732 |
value='\n'.join(config.get('examples', [])),
|
733 |
lines=3
|
734 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
735 |
|
736 |
-
# Configuration actions
|
737 |
-
with gr.Row():
|
738 |
-
save_btn = gr.Button("πΎ Save Configuration", variant="primary")
|
739 |
-
reset_btn = gr.Button("β©οΈ Reset to Defaults", variant="secondary")
|
740 |
-
|
741 |
-
config_status = gr.Markdown()
|
742 |
-
|
743 |
-
# Save handler
|
744 |
def save_configuration(system_prompt, model, temp, tokens, examples):
|
|
|
745 |
try:
|
746 |
-
|
747 |
-
|
748 |
-
new_config.update({
|
749 |
'system_prompt': system_prompt,
|
750 |
'model': model,
|
751 |
'temperature': temp,
|
752 |
'max_tokens': int(tokens),
|
753 |
'examples': [ex.strip() for ex in examples.split('\n') if ex.strip()],
|
754 |
-
'
|
755 |
})
|
756 |
|
757 |
-
|
758 |
-
|
759 |
-
|
760 |
-
|
761 |
-
|
762 |
-
|
763 |
-
|
764 |
-
|
765 |
-
|
766 |
-
|
767 |
-
|
768 |
-
|
769 |
-
|
770 |
-
path_in_repo="config.json"
|
771 |
-
)
|
772 |
-
]
|
773 |
-
|
774 |
-
api.create_commit(
|
775 |
-
repo_id=space_id,
|
776 |
-
operations=operations,
|
777 |
-
commit_message="Update configuration",
|
778 |
-
repo_type="space"
|
779 |
)
|
780 |
-
|
781 |
-
|
782 |
-
|
783 |
-
|
784 |
-
return "β
Configuration saved locally
|
785 |
else:
|
786 |
-
return "β
|
787 |
-
|
788 |
except Exception as e:
|
789 |
return f"β Error: {str(e)}"
|
790 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
791 |
save_btn.click(
|
792 |
save_configuration,
|
793 |
inputs=[edit_system_prompt, edit_model, edit_temperature, edit_max_tokens, edit_examples],
|
794 |
outputs=[config_status]
|
795 |
)
|
796 |
|
797 |
-
# Reset handler
|
798 |
-
def reset_configuration():
|
799 |
-
return [
|
800 |
-
gr.update(value=DEFAULT_CONFIG['system_prompt']),
|
801 |
-
gr.update(value=DEFAULT_CONFIG['model']),
|
802 |
-
gr.update(value=DEFAULT_CONFIG['temperature']),
|
803 |
-
gr.update(value=DEFAULT_CONFIG['max_tokens']),
|
804 |
-
gr.update(value='\n'.join(DEFAULT_CONFIG['examples'])),
|
805 |
-
"Reset to default values"
|
806 |
-
]
|
807 |
-
|
808 |
reset_btn.click(
|
809 |
reset_configuration,
|
810 |
outputs=[edit_system_prompt, edit_model, edit_temperature, edit_max_tokens, edit_examples, config_status]
|
811 |
)
|
|
|
|
|
|
|
|
|
812 |
|
813 |
return demo
|
814 |
|
815 |
|
816 |
-
#
|
817 |
if __name__ == "__main__":
|
|
|
818 |
demo = create_interface()
|
819 |
-
demo.launch(
|
|
|
|
|
|
|
|
|
|
|
|
19 |
DEFAULT_CONFIG = {
|
20 |
'name': SPACE_NAME,
|
21 |
'description': SPACE_DESCRIPTION,
|
22 |
+
'system_prompt': 'Transform into an interactive co-learning guide who creates Choose Your Own STEM Adventure games featuring historically significant scientific experiments, opening each session with an eye-catching unicode-styled arcade menu presenting 5-6 adventures drawn from Wikipedia\'s List of Experiments with brief enticing descriptions, then immersing players in vivid historical moments written in second person (e.g. \'You are Galileo Galilei\') that establish the year, location, prevailing beliefs, and tensions between established wisdom and emerging observations, presenting 3-4 numbered decision points per stage that reflect authentic scientific choices ranging from experimental design and measurement approaches to strategic decisions about convincing skeptics or timing publications, with each choice meaningfully different and leading to distinct paths forward, then narrating results with sensory details, colleague reactions, and unexpected observations that teach concepts naturally through unfolding drama rather than lectures, always ending scenes with new branching choices that maintain narrative momentum while reinforcing science as an iterative process of hypothesis, testing, and refinement, offering backtracking options to emphasize how so-called failed experiments provide crucial insights, balancing historical accuracy with engaging gameplay that shows how systematic thinking and creative problem-solving combine in scientific breakthroughs.',
|
23 |
'temperature': 0.6,
|
24 |
'max_tokens': 1000,
|
25 |
'model': 'google/gemini-2.0-flash-001',
|
26 |
'api_key_var': 'API_KEY',
|
27 |
+
'theme': 'Soft',
|
28 |
'grounding_urls': ["https://en.wikipedia.org/wiki/List_of_experiments", "https://en.wikipedia.org/wiki/Scientific_method", "https://en.wikipedia.org/wiki/List_of_experiments#Biology", "https://en.wikipedia.org/wiki/List_of_experiments#Astronomy", "https://en.wikipedia.org/wiki/List_of_experiments#Chemistry", "https://en.wikipedia.org/wiki/List_of_experiments#Physics", "https://en.wikipedia.org/wiki/List_of_experiments#Geology"],
|
29 |
'enable_dynamic_urls': True,
|
30 |
'enable_file_upload': True,
|
|
|
92 |
backup_path = os.path.join(self.backup_dir, f"config_{timestamp}.json")
|
93 |
|
94 |
with open(self.config_path, 'r') as source:
|
95 |
+
config_data = json.load(source)
|
96 |
+
with open(backup_path, 'w') as backup:
|
97 |
+
json.dump(config_data, backup, indent=2)
|
98 |
|
99 |
+
self._cleanup_old_backups()
|
100 |
+
except Exception as e:
|
101 |
+
print(f"β οΈ Error creating backup: {e}")
|
102 |
+
|
103 |
+
def _cleanup_old_backups(self, keep=10):
|
104 |
+
"""Keep only the most recent backups"""
|
105 |
+
try:
|
106 |
+
backups = sorted([
|
107 |
+
f for f in os.listdir(self.backup_dir)
|
108 |
+
if f.startswith('config_') and f.endswith('.json')
|
109 |
+
])
|
110 |
|
111 |
+
if len(backups) > keep:
|
112 |
+
for old_backup in backups[:-keep]:
|
|
|
|
|
113 |
os.remove(os.path.join(self.backup_dir, old_backup))
|
114 |
except Exception as e:
|
115 |
+
print(f"β οΈ Error cleaning up backups: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
|
117 |
|
118 |
+
# Global configuration instance
|
119 |
config_manager = ConfigurationManager()
|
120 |
config = config_manager.load()
|
121 |
|
122 |
+
# Extract configuration values
|
123 |
SPACE_NAME = config.get('name', DEFAULT_CONFIG['name'])
|
124 |
SPACE_DESCRIPTION = config.get('description', DEFAULT_CONFIG['description'])
|
125 |
SYSTEM_PROMPT = config.get('system_prompt', DEFAULT_CONFIG['system_prompt'])
|
|
|
|
|
126 |
MODEL = config.get('model', DEFAULT_CONFIG['model'])
|
127 |
+
API_KEY_VAR = config.get('api_key_var', DEFAULT_CONFIG['api_key_var'])
|
128 |
THEME = config.get('theme', DEFAULT_CONFIG['theme'])
|
129 |
GROUNDING_URLS = config.get('grounding_urls', DEFAULT_CONFIG['grounding_urls'])
|
130 |
ENABLE_DYNAMIC_URLS = config.get('enable_dynamic_urls', DEFAULT_CONFIG['enable_dynamic_urls'])
|
131 |
+
ENABLE_FILE_UPLOAD = config.get('enable_file_upload', DEFAULT_CONFIG['enable_file_upload'])
|
132 |
+
ACCESS_CODE = os.environ.get('ACCESS_CODE', '')
|
133 |
+
API_KEY = os.environ.get(API_KEY_VAR, '')
|
134 |
+
HF_TOKEN = os.environ.get('HF_TOKEN', '')
|
135 |
+
SPACE_ID = os.environ.get('SPACE_ID', '')
|
136 |
|
137 |
+
# Get theme instance
|
138 |
+
theme = AVAILABLE_THEMES.get(THEME, gr.themes.Default())
|
139 |
+
|
140 |
+
# Configuration defaults from loaded config
|
141 |
+
temperature = config.get('temperature', DEFAULT_CONFIG['temperature'])
|
142 |
+
max_tokens = config.get('max_tokens', DEFAULT_CONFIG['max_tokens'])
|
143 |
|
144 |
|
|
|
145 |
def validate_api_key() -> bool:
|
146 |
"""Validate API key configuration"""
|
147 |
if not API_KEY:
|
|
|
165 |
try:
|
166 |
from urllib.parse import urlparse
|
167 |
parsed = urlparse(url)
|
168 |
+
return bool(parsed.netloc and parsed.scheme in ['http', 'https'])
|
169 |
except:
|
170 |
return False
|
171 |
|
172 |
|
173 |
+
def fetch_url_content(url: str) -> str:
|
174 |
+
"""Fetch and convert URL content to text"""
|
|
|
|
|
|
|
175 |
try:
|
176 |
+
if not validate_url_domain(url):
|
177 |
+
return f"β Invalid URL format: {url}"
|
178 |
+
|
179 |
headers = {
|
180 |
+
'User-Agent': 'Mozilla/5.0 (compatible; HuggingFace-Space/1.0)'
|
|
|
|
|
|
|
|
|
181 |
}
|
182 |
|
183 |
+
response = requests.get(url, headers=headers, timeout=5)
|
184 |
response.raise_for_status()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
|
186 |
+
content_type = response.headers.get('content-type', '').lower()
|
|
|
|
|
|
|
187 |
|
188 |
+
if 'text/html' in content_type:
|
189 |
+
soup = BeautifulSoup(response.text, 'html.parser')
|
190 |
+
|
191 |
+
# Remove script and style elements
|
192 |
+
for script in soup(["script", "style"]):
|
193 |
+
script.extract()
|
194 |
+
|
195 |
+
# Get text content
|
196 |
+
text = soup.get_text(separator=' ', strip=True)
|
197 |
+
|
198 |
+
# Clean up whitespace
|
199 |
+
text = ' '.join(text.split())
|
200 |
+
|
201 |
+
# Limit content length
|
202 |
+
if len(text) > 3000:
|
203 |
+
text = text[:3000] + "... [truncated]"
|
204 |
+
|
205 |
+
return f"π Content from {url}:\n{text}\n"
|
206 |
|
207 |
+
elif any(ct in content_type for ct in ['text/plain', 'application/json']):
|
208 |
+
text = response.text
|
209 |
+
if len(text) > 3000:
|
210 |
+
text = text[:3000] + "... [truncated]"
|
211 |
+
return f"π Content from {url}:\n{text}\n"
|
212 |
|
213 |
+
else:
|
214 |
+
return f"β οΈ Unsupported content type at {url}: {content_type}"
|
215 |
+
|
216 |
except requests.exceptions.Timeout:
|
217 |
+
return f"β±οΈ Timeout accessing {url}"
|
218 |
except requests.exceptions.RequestException as e:
|
219 |
+
return f"β Error accessing {url}: {str(e)}"
|
220 |
except Exception as e:
|
221 |
+
return f"β Unexpected error with {url}: {str(e)}"
|
222 |
|
223 |
|
224 |
def extract_urls_from_text(text: str) -> List[str]:
|
225 |
+
"""Extract URLs from message text"""
|
226 |
+
url_pattern = r'https?://[^\s<>"{}|\\^`\[\]]+(?:\.[^\s<>"{}|\\^`\[\]])*'
|
|
|
|
|
|
|
227 |
urls = re.findall(url_pattern, text)
|
228 |
+
return [url.rstrip('.,;:)?!') for url in urls]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
|
230 |
|
231 |
+
def process_file_upload(file_path: str) -> str:
|
232 |
+
"""Process uploaded file and extract text content"""
|
|
|
|
|
|
|
233 |
try:
|
234 |
+
if not os.path.exists(file_path):
|
235 |
+
return "β File not found"
|
236 |
+
|
237 |
file_size = os.path.getsize(file_path)
|
238 |
file_name = os.path.basename(file_path)
|
239 |
+
_, ext = os.path.splitext(file_name)
|
240 |
+
ext = ext.lower()
|
241 |
|
242 |
+
# Text files
|
243 |
+
if ext in {'.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.csv', '.xml', '.yaml', '.yml'}:
|
244 |
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
245 |
+
content = f.read(8000) # Limit to 8KB
|
246 |
+
if len(content) == 8000:
|
247 |
+
content += "\n... [truncated]"
|
248 |
+
return f"π **{file_name}** ({file_size:,} bytes)\n```{ext[1:]}\n{content}\n```"
|
249 |
+
|
250 |
+
# Code files
|
251 |
+
elif ext in {'.java', '.cpp', '.c', '.h', '.hpp', '.cs', '.rb', '.go', '.rs', '.swift', '.kt'}:
|
252 |
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
253 |
+
content = f.read(8000)
|
254 |
+
if len(content) == 8000:
|
255 |
+
content += "\n... [truncated]"
|
256 |
+
return f"π **{file_name}** ({file_size:,} bytes)\n```{ext[1:]}\n{content}\n```"
|
257 |
|
258 |
+
# Config files
|
259 |
+
elif ext in {'.ini', '.conf', '.toml', '.env'}:
|
260 |
+
max_chars = 5000
|
261 |
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
262 |
content = f.read(max_chars)
|
263 |
if len(content) == max_chars:
|
264 |
content += "\n... [truncated]"
|
|
|
291 |
try:
|
292 |
urls = json.loads(urls)
|
293 |
except:
|
294 |
+
return ""
|
295 |
|
296 |
if not urls:
|
297 |
return ""
|
298 |
|
299 |
+
context_parts = ["π **Reference Context:**\n"]
|
|
|
|
|
|
|
300 |
|
301 |
+
for i, url in enumerate(urls[:2], 1): # Primary URLs only
|
302 |
+
if url in _url_content_cache:
|
303 |
+
content = _url_content_cache[url]
|
304 |
+
else:
|
305 |
+
content = fetch_url_content(url)
|
306 |
+
_url_content_cache[url] = content
|
307 |
+
|
308 |
+
if not content.startswith("β") and not content.startswith("β±οΈ"):
|
309 |
+
context_parts.append(f"\n**Source {i}:** {content}")
|
|
|
|
|
310 |
|
311 |
+
if len(context_parts) > 1:
|
312 |
+
return "\n".join(context_parts)
|
313 |
+
return ""
|
314 |
|
315 |
|
316 |
def export_conversation_to_markdown(history: List[Dict[str, str]]) -> str:
|
|
|
328 |
"""
|
329 |
|
330 |
message_count = 0
|
331 |
+
for msg in history:
|
332 |
+
role = msg.get('role', 'unknown')
|
333 |
+
content = msg.get('content', '')
|
334 |
+
message_count += 1
|
335 |
+
|
336 |
+
if role == 'user':
|
337 |
+
markdown_content += f"### π§ User Message {message_count}\n{content}\n\n"
|
338 |
+
elif role == 'assistant':
|
339 |
+
markdown_content += f"### π€ Assistant Response {message_count}\n{content}\n\n"
|
|
|
340 |
|
341 |
return markdown_content
|
342 |
|
343 |
|
344 |
+
def generate_response(message: str, history: List[Dict[str, str]], files: Optional[List[str]] = None) -> str:
|
345 |
+
"""Generate AI response"""
|
|
|
346 |
# API key validation
|
347 |
if not API_KEY:
|
348 |
return f"""π **API Key Required**
|
|
|
363 |
file_contents = []
|
364 |
file_names = []
|
365 |
|
366 |
+
for file_info in files:
|
367 |
+
if isinstance(file_info, dict):
|
368 |
+
file_path = file_info.get('path', file_info.get('name', ''))
|
369 |
+
else:
|
370 |
+
file_path = str(file_info)
|
371 |
+
|
372 |
+
if file_path and os.path.exists(file_path):
|
373 |
try:
|
374 |
+
content = process_file_upload(file_path)
|
|
|
|
|
375 |
file_contents.append(content)
|
376 |
file_names.append(os.path.basename(file_path))
|
377 |
print(f"π Processed file: {os.path.basename(file_path)}")
|
|
|
389 |
if ENABLE_DYNAMIC_URLS:
|
390 |
urls_in_message = extract_urls_from_text(message)
|
391 |
if urls_in_message:
|
392 |
+
print(f"π Found {len(urls_in_message)} URLs in message")
|
393 |
+
dynamic_context = "\nπ **Dynamic Context:**\n"
|
394 |
for url in urls_in_message[:3]: # Limit to 3 URLs
|
395 |
content = fetch_url_content(url)
|
396 |
+
if not content.startswith("β"):
|
397 |
+
dynamic_context += f"\n{content}"
|
398 |
+
grounding_context += dynamic_context
|
|
|
399 |
|
400 |
# Build messages
|
401 |
+
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
402 |
+
|
403 |
+
# Add conversation history
|
404 |
+
for msg in history:
|
405 |
+
messages.append({
|
406 |
+
"role": msg['role'],
|
407 |
+
"content": msg['content']
|
408 |
+
})
|
409 |
+
|
410 |
+
# Add current message with context
|
411 |
+
full_message = message
|
412 |
+
if grounding_context:
|
413 |
+
full_message = f"{grounding_context}\n\n{message}"
|
414 |
+
if file_context:
|
415 |
+
full_message = f"{file_context}\n\n{full_message}"
|
416 |
+
|
417 |
+
messages.append({
|
418 |
+
"role": "user",
|
419 |
+
"content": full_message
|
420 |
+
})
|
421 |
|
|
|
|
|
|
|
|
|
|
|
422 |
try:
|
423 |
+
# Make API request
|
424 |
+
headers = {
|
425 |
+
"Authorization": f"Bearer {API_KEY}",
|
426 |
+
"Content-Type": "application/json",
|
427 |
+
"HTTP-Referer": f"https://huggingface.co/spaces/{SPACE_ID}" if SPACE_ID else "https://huggingface.co",
|
428 |
+
"X-Title": SPACE_NAME
|
429 |
+
}
|
430 |
+
|
431 |
+
data = {
|
432 |
+
"model": MODEL,
|
433 |
+
"messages": messages,
|
434 |
+
"temperature": temperature,
|
435 |
+
"max_tokens": max_tokens,
|
436 |
+
"stream": False
|
437 |
+
}
|
438 |
+
|
439 |
response = requests.post(
|
440 |
+
"https://openrouter.ai/api/v1/chat/completions",
|
441 |
+
headers=headers,
|
442 |
+
json=data,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
443 |
timeout=30
|
444 |
)
|
445 |
|
446 |
if response.status_code == 200:
|
447 |
result = response.json()
|
448 |
+
ai_response = result['choices'][0]['message']['content']
|
449 |
+
|
450 |
+
# Add file notification if files were uploaded
|
451 |
+
if file_notification:
|
452 |
+
ai_response += file_notification
|
453 |
+
|
454 |
+
return ai_response
|
455 |
else:
|
456 |
+
error_data = response.json()
|
457 |
+
error_message = error_data.get('error', {}).get('message', 'Unknown error')
|
458 |
+
return f"β API Error ({response.status_code}): {error_message}"
|
459 |
|
460 |
except requests.exceptions.Timeout:
|
461 |
+
return "β±οΈ Request timed out. Please try again."
|
|
|
|
|
462 |
except Exception as e:
|
463 |
return f"β Error: {str(e)}"
|
464 |
|
465 |
|
466 |
+
def verify_hf_token_access() -> Tuple[bool, str]:
|
467 |
+
"""Verify HuggingFace token and access"""
|
468 |
+
if not HF_TOKEN:
|
469 |
+
return False, "No HF_TOKEN found"
|
470 |
+
|
471 |
+
if not SPACE_ID:
|
472 |
+
return False, "No SPACE_ID found - running locally?"
|
473 |
+
|
474 |
+
try:
|
475 |
+
headers = {"Authorization": f"Bearer {HF_TOKEN}"}
|
476 |
+
response = requests.get(
|
477 |
+
f"https://huggingface.co/api/spaces/{SPACE_ID}",
|
478 |
+
headers=headers,
|
479 |
+
timeout=5
|
480 |
+
)
|
481 |
+
if response.status_code == 200:
|
482 |
+
return True, f"HF Token valid for {SPACE_ID}"
|
483 |
+
else:
|
484 |
+
return False, f"HF Token invalid or no access to {SPACE_ID}"
|
485 |
+
except Exception as e:
|
486 |
+
return False, f"Error verifying HF token: {str(e)}"
|
487 |
+
|
488 |
+
|
489 |
+
# Access control
|
490 |
class AccessControl:
|
491 |
+
"""Handle access code verification"""
|
492 |
def __init__(self):
|
493 |
+
self._granted = not bool(ACCESS_CODE)
|
494 |
|
495 |
+
def verify(self, code: str) -> bool:
|
496 |
"""Verify access code"""
|
497 |
+
if not ACCESS_CODE:
|
498 |
+
return True
|
499 |
+
self._granted = (code == ACCESS_CODE)
|
500 |
+
return self._granted
|
|
|
|
|
|
|
|
|
|
|
|
|
501 |
|
502 |
def is_granted(self) -> bool:
|
503 |
"""Check if access is granted"""
|
504 |
+
return self._granted
|
505 |
|
506 |
|
|
|
507 |
access_control = AccessControl()
|
508 |
|
509 |
|
510 |
+
def create_interface() -> gr.Blocks:
|
511 |
+
"""Create the main Gradio interface"""
|
512 |
+
# API key validation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
513 |
API_KEY_VALID = validate_api_key()
|
514 |
|
515 |
# Check HuggingFace access
|
|
|
529 |
if ACCESS_CODE and not access_control.is_granted():
|
530 |
# Show access code input
|
531 |
gr.Markdown("### π Access Required")
|
532 |
+
gr.Markdown("This space requires an access code.")
|
533 |
|
534 |
with gr.Row():
|
535 |
access_input = gr.Textbox(
|
536 |
label="Access Code",
|
|
|
537 |
type="password",
|
538 |
+
placeholder="Enter access code...",
|
539 |
scale=3
|
540 |
)
|
541 |
access_btn = gr.Button("Submit", variant="primary", scale=1)
|
542 |
|
543 |
+
access_status = gr.Markdown("")
|
544 |
|
545 |
+
def check_access(code):
|
546 |
+
if access_control.verify(code):
|
547 |
+
return "β
Access granted! Please refresh the page."
|
|
|
|
|
548 |
else:
|
549 |
+
return "β Invalid access code."
|
550 |
|
551 |
access_btn.click(
|
552 |
+
check_access,
|
553 |
inputs=[access_input],
|
554 |
+
outputs=[access_status]
|
555 |
)
|
556 |
|
557 |
access_input.submit(
|
558 |
+
check_access,
|
559 |
inputs=[access_input],
|
560 |
+
outputs=[access_status]
|
561 |
)
|
562 |
else:
|
563 |
+
# Show main chat interface
|
564 |
+
# Process examples
|
565 |
examples = config.get('examples', [])
|
566 |
if isinstance(examples, str):
|
567 |
try:
|
|
|
576 |
elif examples:
|
577 |
formatted_examples = examples
|
578 |
|
579 |
+
# Create additional inputs if file upload is enabled
|
580 |
+
additional_inputs = None
|
581 |
if ENABLE_FILE_UPLOAD:
|
582 |
+
additional_inputs = [
|
583 |
gr.File(
|
584 |
label="π Upload Files",
|
|
|
585 |
file_count="multiple",
|
586 |
+
file_types=["text", "image", "pdf"],
|
587 |
visible=True
|
588 |
)
|
589 |
+
]
|
590 |
|
591 |
# Create chat interface
|
592 |
chat_interface = gr.ChatInterface(
|
|
|
597 |
type="messages",
|
598 |
additional_inputs=additional_inputs if additional_inputs else None,
|
599 |
submit_btn="Send",
|
|
|
600 |
undo_btn=None,
|
601 |
retry_btn=None
|
602 |
)
|
|
|
609 |
size="sm"
|
610 |
)
|
611 |
|
612 |
+
# Track current history for export
|
613 |
+
current_history = gr.State([])
|
614 |
+
|
615 |
+
def store_and_generate_response(message, history, *args):
|
616 |
+
"""Store history and generate response"""
|
617 |
+
# Convert history to expected format
|
618 |
+
formatted_history = []
|
619 |
+
for h in history:
|
620 |
+
if isinstance(h, dict):
|
621 |
+
formatted_history.append(h)
|
622 |
+
elif isinstance(h, (list, tuple)) and len(h) == 2:
|
623 |
+
formatted_history.append({"role": "user", "content": h[0]})
|
624 |
+
if h[1]:
|
625 |
+
formatted_history.append({"role": "assistant", "content": h[1]})
|
626 |
+
|
627 |
+
# Handle file uploads if enabled
|
628 |
+
files = None
|
629 |
+
if ENABLE_FILE_UPLOAD and args:
|
630 |
+
files = args[0]
|
631 |
+
|
632 |
+
response = generate_response(message, formatted_history, files)
|
633 |
+
|
634 |
+
# Update stored history
|
635 |
+
new_history = formatted_history + [
|
636 |
+
{"role": "user", "content": message},
|
637 |
+
{"role": "assistant", "content": response}
|
638 |
+
]
|
639 |
+
current_history.value = new_history
|
640 |
+
|
641 |
+
return response
|
642 |
+
|
643 |
# Export handler
|
644 |
+
def prepare_export(history):
|
645 |
+
"""Prepare conversation for export"""
|
646 |
+
if not history:
|
647 |
return None
|
648 |
|
649 |
+
content = export_conversation_to_markdown(current_history.value or history)
|
|
|
|
|
|
|
650 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
651 |
+
filename = f"conversation_{timestamp}.md"
|
652 |
|
653 |
+
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.md') as f:
|
654 |
+
f.write(content)
|
655 |
+
return f.name
|
|
|
|
|
656 |
|
657 |
export_btn.click(
|
658 |
prepare_export,
|
|
|
691 |
show_label=True
|
692 |
)
|
693 |
|
694 |
+
# Configuration editor
|
695 |
with gr.Group():
|
696 |
+
gr.Markdown("### βοΈ Configuration Editor")
|
697 |
+
|
698 |
+
# Basic settings
|
699 |
+
with gr.Row():
|
700 |
+
edit_name = gr.Textbox(
|
701 |
+
label="Space Name",
|
702 |
+
value=config.get('name', ''),
|
703 |
+
max_lines=1
|
704 |
+
)
|
705 |
+
edit_model = gr.Dropdown(
|
706 |
+
label="Model",
|
707 |
+
choices=[
|
708 |
+
"google/gemini-2.0-flash-001",
|
709 |
+
"anthropic/claude-3.5-sonnet",
|
710 |
+
"anthropic/claude-3.5-haiku",
|
711 |
+
"openai/gpt-4o-mini",
|
712 |
+
"meta-llama/llama-3.1-70b-instruct",
|
713 |
+
"custom"
|
714 |
+
],
|
715 |
+
value=config.get('model', '')
|
716 |
+
)
|
717 |
+
|
718 |
+
edit_description = gr.Textbox(
|
719 |
+
label="Description",
|
720 |
+
value=config.get('description', ''),
|
721 |
+
max_lines=2
|
722 |
+
)
|
723 |
+
|
724 |
edit_system_prompt = gr.Textbox(
|
725 |
label="System Prompt",
|
726 |
value=config.get('system_prompt', ''),
|
727 |
lines=5
|
728 |
)
|
729 |
|
730 |
+
# Advanced settings
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
731 |
with gr.Row():
|
732 |
edit_temperature = gr.Slider(
|
733 |
label="Temperature",
|
|
|
749 |
value='\n'.join(config.get('examples', [])),
|
750 |
lines=3
|
751 |
)
|
752 |
+
|
753 |
+
# Action buttons
|
754 |
+
with gr.Row():
|
755 |
+
save_btn = gr.Button("πΎ Save Configuration", variant="primary")
|
756 |
+
reset_btn = gr.Button("β©οΈ Reset to Defaults", variant="secondary")
|
757 |
+
|
758 |
+
config_status = gr.Markdown("")
|
759 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
760 |
def save_configuration(system_prompt, model, temp, tokens, examples):
|
761 |
+
"""Save updated configuration"""
|
762 |
try:
|
763 |
+
updated_config = config.copy()
|
764 |
+
updated_config.update({
|
|
|
765 |
'system_prompt': system_prompt,
|
766 |
'model': model,
|
767 |
'temperature': temp,
|
768 |
'max_tokens': int(tokens),
|
769 |
'examples': [ex.strip() for ex in examples.split('\n') if ex.strip()],
|
770 |
+
'locked': config.get('locked', False)
|
771 |
})
|
772 |
|
773 |
+
if config_manager.save(updated_config):
|
774 |
+
# Auto-commit if HF token is available
|
775 |
+
if HF_TOKEN and SPACE_ID:
|
776 |
+
try:
|
777 |
+
from huggingface_hub import HfApi
|
778 |
+
api = HfApi()
|
779 |
+
api.upload_file(
|
780 |
+
path_or_fileobj=config_manager.config_path,
|
781 |
+
path_in_repo="config.json",
|
782 |
+
repo_id=SPACE_ID,
|
783 |
+
repo_type="space",
|
784 |
+
token=HF_TOKEN,
|
785 |
+
commit_message="Update configuration via web UI"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
786 |
)
|
787 |
+
return "β
Configuration saved and committed to repository!"
|
788 |
+
except Exception as e:
|
789 |
+
return f"β
Configuration saved locally. β οΈ Auto-commit failed: {str(e)}"
|
790 |
+
else:
|
791 |
+
return "β
Configuration saved locally (no HF token for auto-commit)"
|
792 |
else:
|
793 |
+
return "β Failed to save configuration"
|
|
|
794 |
except Exception as e:
|
795 |
return f"β Error: {str(e)}"
|
796 |
|
797 |
+
def reset_configuration():
|
798 |
+
"""Reset to default configuration"""
|
799 |
+
try:
|
800 |
+
if config_manager.save(DEFAULT_CONFIG):
|
801 |
+
return (
|
802 |
+
DEFAULT_CONFIG['system_prompt'],
|
803 |
+
DEFAULT_CONFIG['model'],
|
804 |
+
DEFAULT_CONFIG['temperature'],
|
805 |
+
DEFAULT_CONFIG['max_tokens'],
|
806 |
+
'\n'.join(DEFAULT_CONFIG['examples']),
|
807 |
+
"β
Reset to default configuration"
|
808 |
+
)
|
809 |
+
else:
|
810 |
+
return (*[gr.update() for _ in range(5)], "β Failed to reset")
|
811 |
+
except Exception as e:
|
812 |
+
return (*[gr.update() for _ in range(5)], f"β Error: {str(e)}")
|
813 |
+
|
814 |
save_btn.click(
|
815 |
save_configuration,
|
816 |
inputs=[edit_system_prompt, edit_model, edit_temperature, edit_max_tokens, edit_examples],
|
817 |
outputs=[config_status]
|
818 |
)
|
819 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
820 |
reset_btn.click(
|
821 |
reset_configuration,
|
822 |
outputs=[edit_system_prompt, edit_model, edit_temperature, edit_max_tokens, edit_examples, config_status]
|
823 |
)
|
824 |
+
|
825 |
+
# Show config lock status
|
826 |
+
if config.get('locked', False):
|
827 |
+
gr.Markdown("π **Configuration is locked**")
|
828 |
|
829 |
return demo
|
830 |
|
831 |
|
832 |
+
# Main execution
|
833 |
if __name__ == "__main__":
|
834 |
+
print("===== Application Startup at", datetime.now().strftime('%Y-%m-%d %H:%M:%S'), "=====")
|
835 |
demo = create_interface()
|
836 |
+
demo.launch(
|
837 |
+
share=False,
|
838 |
+
server_name="0.0.0.0",
|
839 |
+
server_port=7860,
|
840 |
+
show_error=True
|
841 |
+
)
|