linxinhua commited on
Commit
6b97fd2
·
verified ·
1 Parent(s): 0ee193b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +180 -112
app.py CHANGED
@@ -3,6 +3,8 @@ import os
3
  import csv
4
  from datetime import datetime, timedelta
5
  from huggingface_hub import Repository
 
 
6
 
7
  # Configuration
8
  DATA_STORAGE_REPO = "CIV3283/Data_Storage"
@@ -12,6 +14,83 @@ MIN_IDLE_MINUTES = 10 # Minimum idle time required for space assignment
12
  ALLOCATION_RECORD_FILE = "allocation_records.csv" # New file for tracking allocations
13
  ALLOCATION_LOCK_DURATION = 5 # Lock duration in minutes
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  # Environment variables
16
  HF_HUB_TOKEN = os.environ.get("HF_HUB_TOKEN", None)
17
  if HF_HUB_TOKEN is None:
@@ -123,7 +202,7 @@ def get_last_activity_time(csv_file_path):
123
  return datetime.min
124
 
125
  def read_allocation_records(repo_dir):
126
- """Read and parse allocation records, returning only non-expired allocations (similar to query log reading)"""
127
  allocation_file = os.path.join(repo_dir, ALLOCATION_RECORD_FILE)
128
  current_time = datetime.now()
129
  active_allocations = {}
@@ -193,7 +272,7 @@ def read_allocation_records(repo_dir):
193
  return active_allocations
194
 
195
  def write_allocation_record(space_name, student_id, repo_dir):
196
- """Write a new allocation record to the file (similar to query log approach)"""
197
  allocation_file = os.path.join(repo_dir, ALLOCATION_RECORD_FILE)
198
  current_time = datetime.now()
199
  expires_at = current_time + timedelta(minutes=ALLOCATION_LOCK_DURATION)
@@ -220,7 +299,7 @@ def write_allocation_record(space_name, student_id, repo_dir):
220
  except StopIteration:
221
  pass # Empty file or no header
222
 
223
- # Append new allocation record (similar to query log writing)
224
  with open(allocation_file, 'w', newline='', encoding='utf-8') as f:
225
  writer = csv.writer(f)
226
  # Write header
@@ -244,7 +323,7 @@ def write_allocation_record(space_name, student_id, repo_dir):
244
  return False
245
 
246
  def simple_push_allocation_record(repo, space_name, student_id):
247
- """Simple push allocation record using Repository API (similar to how query logs are handled)"""
248
  try:
249
  # Use Repository's push_to_hub method instead of git commands
250
  commit_message = f"Allocate {space_name} to student {student_id}"
@@ -288,12 +367,17 @@ def analyze_space_activity(available_spaces, repo_dir):
288
  status = f"Idle for {idle_minutes:.1f} minutes"
289
  last_activity_str = last_activity.strftime('%Y-%m-%d %H:%M:%S')
290
 
291
- # Check if space is recently allocated
292
- is_recently_allocated = space_name in active_allocations
293
- if is_recently_allocated:
294
  alloc_info = active_allocations[space_name]
295
  minutes_until_free = (alloc_info['expires_at'] - current_time).total_seconds() / 60
296
- status += f" (Recently allocated, free in {minutes_until_free:.1f} min)"
 
 
 
 
 
297
 
298
  space_activity.append({
299
  'space_name': space_name,
@@ -301,7 +385,8 @@ def analyze_space_activity(available_spaces, repo_dir):
301
  'last_activity_str': last_activity_str,
302
  'idle_minutes': idle_minutes,
303
  'status': status,
304
- 'is_recently_allocated': is_recently_allocated
 
305
  })
306
 
307
  print(f"[analyze_space_activity] {space_name}: {status}")
@@ -315,6 +400,14 @@ def create_status_display(space_activity):
315
  """Create formatted status display for all spaces with proper line breaks"""
316
  status_display = "📊 **Current Space Status (sorted by availability):**<br><br>"
317
 
 
 
 
 
 
 
 
 
318
  for i, space in enumerate(space_activity, 1):
319
  status_display += f"{i}. **{space['space_name']}**<br>"
320
  status_display += f"&nbsp;&nbsp;&nbsp;• Status: {space['status']}<br>"
@@ -322,74 +415,96 @@ def create_status_display(space_activity):
322
 
323
  return status_display
324
 
325
- def select_space_with_boundary_check(space_activity, student_id, repo):
326
- """Select space with strict boundary conditions and allocation recording"""
327
 
328
- # Filter out recently allocated spaces
329
- available_spaces = [s for s in space_activity
330
- if s['idle_minutes'] >= MIN_IDLE_MINUTES and not s['is_recently_allocated']]
331
 
332
- if not available_spaces:
333
- # Check what's preventing allocation
 
 
 
 
 
 
 
 
 
 
 
334
  idle_spaces = [s for s in space_activity if s['idle_minutes'] >= MIN_IDLE_MINUTES]
335
- recently_allocated_spaces = [s for s in space_activity if s['is_recently_allocated']]
 
336
 
337
- if idle_spaces and not recently_allocated_spaces:
338
- # All spaces are busy (used within the last 30 minutes)
339
- most_idle = space_activity[0] if space_activity else None
340
-
341
- if most_idle:
342
- if most_idle['idle_minutes'] == float('inf'):
343
- idle_info = "Never used"
344
- else:
345
- idle_info = f"{most_idle['idle_minutes']:.1f} minutes ago"
346
-
347
- error_msg = (
348
- f"🚫 **All learning assistants are currently busy**\n\n"
349
- f"All spaces have been used within the last {MIN_IDLE_MINUTES} minutes.\n\n"
350
- f"**Current Status:**\n"
351
- f" Most idle space: {most_idle['space_name']}\n"
352
- f"• Last used: {idle_info}\n\n"
353
- f"**Please try again in a few minutes.**"
354
- )
355
- else:
356
- error_msg = "🚫 No learning assistant spaces are available at the moment."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  else:
358
- # Spaces are recently allocated
359
  error_msg = (
360
- f"🚫 **All learning assistants are currently busy or recently allocated**\n\n"
361
- f"Some spaces are being assigned to other students right now.\n\n"
362
- f"**Please try again in 1-2 minutes.**"
363
  )
364
-
365
- print(f"[select_space_with_boundary_check] No available spaces: {error_msg}")
366
- raise gr.Error(error_msg, duration=10)
367
 
368
- # Select the most idle space
369
- selected_space = available_spaces[0]
370
- print(f"[select_space_with_boundary_check] Selected space: {selected_space['space_name']}")
371
 
372
- # Record the allocation BEFORE building the response
373
- write_success = write_allocation_record(selected_space['space_name'], student_id, repo.local_dir)
374
  if write_success:
375
- # Try to push to remote (non-blocking)
376
- push_success = simple_push_allocation_record(repo, selected_space['space_name'], student_id)
377
  if not push_success:
378
- print(f"[select_space_with_boundary_check] Warning: Allocation recorded locally but not synced to remote")
379
  else:
380
- print(f"[select_space_with_boundary_check] Warning: Failed to record allocation")
381
- # Continue anyway - worst case is potential double allocation, but user gets service
382
 
383
- # Create status display
384
  status_display = create_status_display(space_activity)
 
385
 
386
- # Build redirect URL
387
- redirect_url = f"https://huggingface.co/spaces/CIV3283/{selected_space['space_name']}/?check={student_id}"
388
 
389
  return redirect_to_space(redirect_url, selected_space, status_display)
390
 
391
  def redirect_to_space(redirect_url, selected_space, status_display):
392
- """Display redirect information with manual click option (HF Spaces can't auto-redirect)"""
393
 
394
  if selected_space['idle_minutes'] == float('inf'):
395
  idle_info = "Never used (completely fresh)"
@@ -435,7 +550,7 @@ def redirect_to_space(redirect_url, selected_space, status_display):
435
  {status_display}
436
  </div>
437
  <p style="margin-bottom: 0; color: #666; font-size: 14px; margin-top: 15px;">
438
- <strong>Selection Algorithm:</strong> Spaces idle for ≥{MIN_IDLE_MINUTES} minutes and not recently allocated are eligible. The most idle space is automatically selected for optimal load distribution.
439
  </p>
440
  </div>
441
 
@@ -450,8 +565,8 @@ def redirect_to_space(redirect_url, selected_space, status_display):
450
  return gr.HTML(redirect_html)
451
 
452
  def load_balance_user(student_id):
453
- """Main load balancing function with allocation recording"""
454
- print(f"[load_balance_user] Starting load balancing for student ID: {student_id}")
455
 
456
  # Initialize connection to data storage
457
  repo = init_data_storage_connection()
@@ -464,11 +579,11 @@ def load_balance_user(student_id):
464
  if not available_spaces:
465
  raise gr.Error("🚫 No student learning assistant spaces found in the system. Please contact your instructor.", duration=8)
466
 
467
- # Analyze space activity (including allocation records)
468
  space_activity = analyze_space_activity(available_spaces, LOCAL_DATA_DIR)
469
 
470
- # Select space with boundary checking and allocation recording
471
- return select_space_with_boundary_check(space_activity, student_id, repo)
472
 
473
  def get_url_params(request: gr.Request):
474
  """Extract URL parameters from request"""
@@ -482,51 +597,4 @@ def get_url_params(request: gr.Request):
482
  return "Load Distributor", None
483
 
484
  def handle_user_access(request: gr.Request):
485
- """Handle user access and perform load balancing"""
486
- title, check_id = get_url_params(request)
487
-
488
- if not check_id:
489
- # No student ID provided
490
- error_html = """
491
- <div style="max-width: 600px; margin: 50px auto; padding: 30px; text-align: center;
492
- background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 12px;">
493
- <h2 style="color: #856404;">⚠️ Invalid Access</h2>
494
- <p style="color: #856404; font-size: 16px; line-height: 1.6;">
495
- This load distributor requires a valid student ID parameter.<br><br>
496
- <strong>Please access this system through the official link provided in Moodle.</strong>
497
- </p>
498
- </div>
499
- """
500
- return title, gr.HTML(error_html)
501
-
502
- # Valid student ID - perform load balancing
503
- try:
504
- result = load_balance_user(check_id)
505
- return title, result
506
- except Exception as e:
507
- # Handle any errors during load balancing
508
- error_html = f"""
509
- <div style="max-width: 600px; margin: 50px auto; padding: 30px; text-align: center;
510
- background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 12px;">
511
- <h2 style="color: #721c24;">🚫 Load Balancing Error</h2>
512
- <p style="color: #721c24; font-size: 16px; line-height: 1.6;">
513
- {str(e)}<br><br>
514
- Please try again in a few moments or contact your instructor if the problem persists.
515
- </p>
516
- </div>
517
- """
518
- return title, gr.HTML(error_html)
519
-
520
- # Create Gradio interface
521
- with gr.Blocks(title="CIV3283 Load Distributor") as interface:
522
- title_display = gr.Markdown("# 🔄 CIV3283 Learning Assistant Load Distributor", elem_id="title")
523
- content_display = gr.HTML("")
524
-
525
- # Initialize on page load
526
- interface.load(
527
- fn=handle_user_access,
528
- outputs=[title_display, content_display]
529
- )
530
-
531
- if __name__ == "__main__":
532
- interface.launch()
 
3
  import csv
4
  from datetime import datetime, timedelta
5
  from huggingface_hub import Repository
6
+ import threading
7
+ import time
8
 
9
  # Configuration
10
  DATA_STORAGE_REPO = "CIV3283/Data_Storage"
 
14
  ALLOCATION_RECORD_FILE = "allocation_records.csv" # New file for tracking allocations
15
  ALLOCATION_LOCK_DURATION = 5 # Lock duration in minutes
16
 
17
+ # 新增:本地防撞车机制
18
+ class LocalAllocationTracker:
19
+ def __init__(self):
20
+ self._recent_allocations = {} # {space_name: {'student_id': str, 'timestamp': datetime}}
21
+ self._lock = threading.Lock()
22
+ self._cleanup_interval = 60 # 清理间隔(秒)
23
+ self._allocation_ttl = 30 # 本地分配记录的生存时间(秒)
24
+
25
+ # 启动后台清理线程
26
+ self._start_cleanup_thread()
27
+
28
+ def _start_cleanup_thread(self):
29
+ """启动后台线程定期清理过期的本地分配记录"""
30
+ def cleanup_worker():
31
+ while True:
32
+ time.sleep(self._cleanup_interval)
33
+ self._cleanup_expired_allocations()
34
+
35
+ cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
36
+ cleanup_thread.start()
37
+ print("[LocalAllocationTracker] Background cleanup thread started")
38
+
39
+ def _cleanup_expired_allocations(self):
40
+ """清理过期的本地分配记录"""
41
+ with self._lock:
42
+ current_time = datetime.now()
43
+ expired_spaces = []
44
+
45
+ for space_name, alloc_info in self._recent_allocations.items():
46
+ if (current_time - alloc_info['timestamp']).total_seconds() > self._allocation_ttl:
47
+ expired_spaces.append(space_name)
48
+
49
+ for space_name in expired_spaces:
50
+ del self._recent_allocations[space_name]
51
+ print(f"[LocalAllocationTracker] Cleaned up expired allocation: {space_name}")
52
+
53
+ def is_recently_allocated_locally(self, space_name):
54
+ """检查空间是否在本地被最近分配过"""
55
+ with self._lock:
56
+ if space_name not in self._recent_allocations:
57
+ return False, None
58
+
59
+ alloc_info = self._recent_allocations[space_name]
60
+ current_time = datetime.now()
61
+ elapsed_seconds = (current_time - alloc_info['timestamp']).total_seconds()
62
+
63
+ if elapsed_seconds > self._allocation_ttl:
64
+ # 过期了,删除记录
65
+ del self._recent_allocations[space_name]
66
+ print(f"[LocalAllocationTracker] Expired local allocation removed: {space_name}")
67
+ return False, None
68
+
69
+ print(f"[LocalAllocationTracker] Space {space_name} recently allocated locally to {alloc_info['student_id']} ({elapsed_seconds:.1f}s ago)")
70
+ return True, alloc_info['student_id']
71
+
72
+ def record_local_allocation(self, space_name, student_id):
73
+ """记录本地分配"""
74
+ with self._lock:
75
+ self._recent_allocations[space_name] = {
76
+ 'student_id': student_id,
77
+ 'timestamp': datetime.now()
78
+ }
79
+ print(f"[LocalAllocationTracker] Locally recorded allocation: {space_name} -> {student_id}")
80
+
81
+ def get_recent_allocations_summary(self):
82
+ """获取最近本地分配的摘要(用于调试)"""
83
+ with self._lock:
84
+ current_time = datetime.now()
85
+ summary = []
86
+ for space_name, alloc_info in self._recent_allocations.items():
87
+ elapsed = (current_time - alloc_info['timestamp']).total_seconds()
88
+ summary.append(f"{space_name} -> {alloc_info['student_id']} ({elapsed:.1f}s ago)")
89
+ return summary
90
+
91
+ # 全局实例
92
+ local_tracker = LocalAllocationTracker()
93
+
94
  # Environment variables
95
  HF_HUB_TOKEN = os.environ.get("HF_HUB_TOKEN", None)
96
  if HF_HUB_TOKEN is None:
 
202
  return datetime.min
203
 
204
  def read_allocation_records(repo_dir):
205
+ """Read and parse allocation records, returning only non-expired allocations"""
206
  allocation_file = os.path.join(repo_dir, ALLOCATION_RECORD_FILE)
207
  current_time = datetime.now()
208
  active_allocations = {}
 
272
  return active_allocations
273
 
274
  def write_allocation_record(space_name, student_id, repo_dir):
275
+ """Write a new allocation record to the file"""
276
  allocation_file = os.path.join(repo_dir, ALLOCATION_RECORD_FILE)
277
  current_time = datetime.now()
278
  expires_at = current_time + timedelta(minutes=ALLOCATION_LOCK_DURATION)
 
299
  except StopIteration:
300
  pass # Empty file or no header
301
 
302
+ # Append new allocation record
303
  with open(allocation_file, 'w', newline='', encoding='utf-8') as f:
304
  writer = csv.writer(f)
305
  # Write header
 
323
  return False
324
 
325
  def simple_push_allocation_record(repo, space_name, student_id):
326
+ """Simple push allocation record using Repository API"""
327
  try:
328
  # Use Repository's push_to_hub method instead of git commands
329
  commit_message = f"Allocate {space_name} to student {student_id}"
 
367
  status = f"Idle for {idle_minutes:.1f} minutes"
368
  last_activity_str = last_activity.strftime('%Y-%m-%d %H:%M:%S')
369
 
370
+ # Check if space is recently allocated (remote)
371
+ is_recently_allocated_remote = space_name in active_allocations
372
+ if is_recently_allocated_remote:
373
  alloc_info = active_allocations[space_name]
374
  minutes_until_free = (alloc_info['expires_at'] - current_time).total_seconds() / 60
375
+ status += f" (Recently allocated remotely, free in {minutes_until_free:.1f} min)"
376
+
377
+ # Check if space is recently allocated (local)
378
+ is_recently_allocated_local, local_student = local_tracker.is_recently_allocated_locally(space_name)
379
+ if is_recently_allocated_local:
380
+ status += f" (Recently allocated locally to {local_student})"
381
 
382
  space_activity.append({
383
  'space_name': space_name,
 
385
  'last_activity_str': last_activity_str,
386
  'idle_minutes': idle_minutes,
387
  'status': status,
388
+ 'is_recently_allocated_remote': is_recently_allocated_remote,
389
+ 'is_recently_allocated_local': is_recently_allocated_local
390
  })
391
 
392
  print(f"[analyze_space_activity] {space_name}: {status}")
 
400
  """Create formatted status display for all spaces with proper line breaks"""
401
  status_display = "📊 **Current Space Status (sorted by availability):**<br><br>"
402
 
403
+ # 显示本地分配记录摘要
404
+ local_summary = local_tracker.get_recent_allocations_summary()
405
+ if local_summary:
406
+ status_display += "🔒 **Recent Local Allocations:**<br>"
407
+ for alloc in local_summary:
408
+ status_display += f"&nbsp;&nbsp;&nbsp;• {alloc}<br>"
409
+ status_display += "<br>"
410
+
411
  for i, space in enumerate(space_activity, 1):
412
  status_display += f"{i}. **{space['space_name']}**<br>"
413
  status_display += f"&nbsp;&nbsp;&nbsp;• Status: {space['status']}<br>"
 
415
 
416
  return status_display
417
 
418
+ def select_space_with_enhanced_collision_avoidance(space_activity, student_id, repo):
419
+ """改进的空间选择函数,包含本地防撞车机制"""
420
 
421
+ print(f"[select_space_with_enhanced_collision_avoidance] Starting selection for student: {student_id}")
 
 
422
 
423
+ # 第一步:过滤掉不符合基本条件的空间
424
+ basic_available_spaces = []
425
+ for space in space_activity:
426
+ # 检查基本条件
427
+ if (space['idle_minutes'] >= MIN_IDLE_MINUTES and
428
+ not space['is_recently_allocated_remote'] and
429
+ not space['is_recently_allocated_local']):
430
+ basic_available_spaces.append(space)
431
+
432
+ print(f"[select_space_with_enhanced_collision_avoidance] Basic available spaces: {len(basic_available_spaces)}")
433
+
434
+ if not basic_available_spaces:
435
+ # 生成详细的错误信息
436
  idle_spaces = [s for s in space_activity if s['idle_minutes'] >= MIN_IDLE_MINUTES]
437
+ remote_allocated = [s for s in space_activity if s['is_recently_allocated_remote']]
438
+ local_allocated = [s for s in space_activity if s['is_recently_allocated_local']]
439
 
440
+ error_parts = []
441
+ if not idle_spaces:
442
+ error_parts.append(f"all spaces used within {MIN_IDLE_MINUTES} minutes")
443
+ if remote_allocated:
444
+ error_parts.append(f"{len(remote_allocated)} spaces remotely allocated")
445
+ if local_allocated:
446
+ error_parts.append(f"{len(local_allocated)} spaces locally allocated")
447
+
448
+ error_msg = (
449
+ f"🚫 **All learning assistants are currently busy**\n\n"
450
+ f"Blocking conditions: {', '.join(error_parts)}\n\n"
451
+ f"**Please try again in 1-2 minutes.**"
452
+ )
453
+
454
+ print(f"[select_space_with_enhanced_collision_avoidance] No available spaces: {error_msg}")
455
+ raise gr.Error(error_msg, duration=10)
456
+
457
+ # 第二步:选择最优空间并进行最终验证
458
+ selected_space = basic_available_spaces[0] # 已经按idle_time排序
459
+ space_name = selected_space['space_name']
460
+
461
+ print(f"[select_space_with_enhanced_collision_avoidance] Preliminary selection: {space_name}")
462
+
463
+ # 第三步:最终防撞车检查 - 再次验证本地分配状态
464
+ is_local_conflict, conflicting_student = local_tracker.is_recently_allocated_locally(space_name)
465
+ if is_local_conflict:
466
+ print(f"[select_space_with_enhanced_collision_avoidance] COLLISION DETECTED! {space_name} recently allocated to {conflicting_student}")
467
+
468
+ # 寻找替代空间
469
+ alternative_spaces = [s for s in basic_available_spaces[1:]
470
+ if not local_tracker.is_recently_allocated_locally(s['space_name'])[0]]
471
+
472
+ if alternative_spaces:
473
+ selected_space = alternative_spaces[0]
474
+ space_name = selected_space['space_name']
475
+ print(f"[select_space_with_enhanced_collision_avoidance] Using alternative space: {space_name}")
476
  else:
 
477
  error_msg = (
478
+ f"🚫 **Collision detected and no alternatives available**\n\n"
479
+ f"The system detected a potential conflict with another student's allocation.\n\n"
480
+ f"**Please try again in 10-15 seconds.**"
481
  )
482
+ print(f"[select_space_with_enhanced_collision_avoidance] No alternatives available")
483
+ raise gr.Error(error_msg, duration=8)
 
484
 
485
+ # 第四步:立即记录本地分配(在写入文件之前)
486
+ local_tracker.record_local_allocation(space_name, student_id)
487
+ print(f"[select_space_with_enhanced_collision_avoidance] Local allocation recorded BEFORE file write")
488
 
489
+ # 第五步:记录到文件和远程
490
+ write_success = write_allocation_record(space_name, student_id, repo.local_dir)
491
  if write_success:
492
+ push_success = simple_push_allocation_record(repo, space_name, student_id)
 
493
  if not push_success:
494
+ print(f"[select_space_with_enhanced_collision_avoidance] Warning: Allocation recorded locally but not synced to remote")
495
  else:
496
+ print(f"[select_space_with_enhanced_collision_avoidance] Warning: Failed to record allocation to file")
 
497
 
498
+ # 第六步:生成结果
499
  status_display = create_status_display(space_activity)
500
+ redirect_url = f"https://huggingface.co/spaces/CIV3283/{space_name}/?check={student_id}"
501
 
502
+ print(f"[select_space_with_enhanced_collision_avoidance] Final allocation: {space_name} -> {student_id}")
 
503
 
504
  return redirect_to_space(redirect_url, selected_space, status_display)
505
 
506
  def redirect_to_space(redirect_url, selected_space, status_display):
507
+ """Display redirect information with manual click option"""
508
 
509
  if selected_space['idle_minutes'] == float('inf'):
510
  idle_info = "Never used (completely fresh)"
 
550
  {status_display}
551
  </div>
552
  <p style="margin-bottom: 0; color: #666; font-size: 14px; margin-top: 15px;">
553
+ <strong>Enhanced Selection Algorithm:</strong> Spaces idle for ≥{MIN_IDLE_MINUTES} minutes, not remotely allocated, and not locally allocated are eligible. The system includes real-time collision avoidance to prevent multiple students from being assigned to the same space.
554
  </p>
555
  </div>
556
 
 
565
  return gr.HTML(redirect_html)
566
 
567
  def load_balance_user(student_id):
568
+ """Main load balancing function with enhanced collision avoidance"""
569
+ print(f"[load_balance_user] Starting enhanced load balancing for student ID: {student_id}")
570
 
571
  # Initialize connection to data storage
572
  repo = init_data_storage_connection()
 
579
  if not available_spaces:
580
  raise gr.Error("🚫 No student learning assistant spaces found in the system. Please contact your instructor.", duration=8)
581
 
582
+ # Analyze space activity (including both remote and local allocation records)
583
  space_activity = analyze_space_activity(available_spaces, LOCAL_DATA_DIR)
584
 
585
+ # Select space with enhanced collision avoidance
586
+ return select_space_with_enhanced_collision_avoidance(space_activity, student_id, repo)
587
 
588
  def get_url_params(request: gr.Request):
589
  """Extract URL parameters from request"""
 
597
  return "Load Distributor", None
598
 
599
  def handle_user_access(request: gr.Request):
600
+ """Handle user access an