linxinhua commited on
Commit
ddb0df6
Β·
verified Β·
1 Parent(s): 52520b1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +182 -66
app.py CHANGED
@@ -1,7 +1,7 @@
1
  import gradio as gr
2
  import os
3
  import csv
4
- from datetime import datetime
5
  from huggingface_hub import Repository
6
 
7
  # Configuration
@@ -9,6 +9,8 @@ DATA_STORAGE_REPO = "CIV3283/Data_Storage"
9
  DATA_BRANCH_NAME = "data_branch"
10
  LOCAL_DATA_DIR = "temp_data_storage"
11
  MIN_IDLE_MINUTES = 10 # Minimum idle time required for space assignment
 
 
12
 
13
  # Environment variables
14
  HF_HUB_TOKEN = os.environ.get("HF_HUB_TOKEN", None)
@@ -117,11 +119,118 @@ def get_last_activity_time(csv_file_path):
117
  print(f"[get_last_activity_time] Traceback: {traceback.format_exc()}")
118
  return datetime.min
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  def analyze_space_activity(available_spaces, repo_dir):
121
  """Analyze activity status of all spaces and return sorted list"""
122
  space_activity = []
123
  current_time = datetime.now()
124
 
 
 
 
125
  print(f"[analyze_space_activity] Analyzing {len(available_spaces)} spaces...")
126
 
127
  for space_name in available_spaces:
@@ -138,12 +247,20 @@ def analyze_space_activity(available_spaces, repo_dir):
138
  status = f"Idle for {idle_minutes:.1f} minutes"
139
  last_activity_str = last_activity.strftime('%Y-%m-%d %H:%M:%S')
140
 
 
 
 
 
 
 
 
141
  space_activity.append({
142
  'space_name': space_name,
143
  'last_activity': last_activity,
144
  'last_activity_str': last_activity_str,
145
  'idle_minutes': idle_minutes,
146
- 'status': status
 
147
  })
148
 
149
  print(f"[analyze_space_activity] {space_name}: {status}")
@@ -164,44 +281,63 @@ def create_status_display(space_activity):
164
 
165
  return status_display
166
 
167
- def select_space_with_boundary_check(space_activity, student_id):
168
- """Select space with strict boundary conditions"""
169
-
170
- # Create and display current status
171
- status_display = create_status_display(space_activity)
172
- print(f"[select_space_with_boundary_check] Space analysis:\n{status_display}")
173
 
174
- # Check for available spaces (idle for at least MIN_IDLE_MINUTES)
175
- available_spaces = [s for s in space_activity if s['idle_minutes'] >= MIN_IDLE_MINUTES]
 
176
 
177
  if not available_spaces:
178
- # All spaces are busy (used within the last 30 minutes)
179
- most_idle = space_activity[0] if space_activity else None
 
180
 
181
- if most_idle:
182
- if most_idle['idle_minutes'] == float('inf'):
183
- idle_info = "Never used"
184
- else:
185
- idle_info = f"{most_idle['idle_minutes']:.1f} minutes ago"
186
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
  error_msg = (
188
- f"🚫 **All learning assistants are currently busy**\n\n"
189
- f"All spaces have been used within the last {MIN_IDLE_MINUTES} minutes.\n\n"
190
- f"**Current Status:**\n"
191
- f"β€’ Most idle space: {most_idle['space_name']}\n"
192
- f"β€’ Last used: {idle_info}\n\n"
193
- f"**Please try again in a few minutes.**"
194
  )
195
- else:
196
- error_msg = "🚫 No learning assistant spaces are available at the moment."
197
 
198
- print(f"[select_space_with_boundary_check] All spaces busy: {error_msg}")
199
  raise gr.Error(error_msg, duration=10)
200
 
201
  # Select the most idle space
202
  selected_space = available_spaces[0]
203
  print(f"[select_space_with_boundary_check] Selected space: {selected_space['space_name']}")
204
 
 
 
 
 
 
 
 
 
 
 
205
  # Build redirect URL
206
  redirect_url = f"https://huggingface.co/spaces/CIV3283/{selected_space['space_name']}/?check={student_id}"
207
 
@@ -215,6 +351,7 @@ def redirect_to_space(redirect_url, selected_space, status_display):
215
  else:
216
  idle_info = f"{selected_space['idle_minutes']:.1f} minutes"
217
 
 
218
  redirect_html = f"""
219
  <div style="max-width: 900px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
220
  <div style="text-align: center; margin-bottom: 30px; padding: 25px; background: linear-gradient(135deg, #28a745, #20c997); color: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
@@ -225,20 +362,10 @@ def redirect_to_space(redirect_url, selected_space, status_display):
225
  </p>
226
  </div>
227
 
228
- <div style="background: #f8f9fa; padding: 25px; border-radius: 12px; margin-bottom: 25px; border-left: 4px solid #28a745;">
229
- <h3 style="margin-top: 0; color: #333;">πŸ“Š Space Selection Analysis</h3>
230
- <div style="background: white; padding: 20px; border-radius: 8px; font-size: 14px; line-height: 1.8; border: 1px solid #e9ecef;">
231
- {status_display}
232
- </div>
233
- <p style="margin-bottom: 0; color: #666; font-size: 14px; margin-top: 15px;">
234
- <strong>Selection Algorithm:</strong> Spaces idle for β‰₯{MIN_IDLE_MINUTES} minutes are eligible. The most idle space is automatically selected for optimal load distribution.
235
- </p>
236
- </div>
237
-
238
  <div style="text-align: center; margin-bottom: 30px; padding: 25px; background: linear-gradient(135deg, #2196f3, #1976d2); color: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(33,150,243,0.3);">
239
  <h2 style="margin-top: 0; color: white;">πŸš€ Access Your Learning Assistant</h2>
240
  <p style="color: rgba(255,255,255,0.9); font-size: 16px; margin-bottom: 25px;">
241
- Click the button below or copy the URL to access your assigned learning assistant.
242
  </p>
243
 
244
  <a href="{redirect_url}"
@@ -249,33 +376,22 @@ def redirect_to_space(redirect_url, selected_space, status_display):
249
  transition: all 0.3s ease; margin-bottom: 20px;"
250
  onmouseover="this.style.background='rgba(255,255,255,0.25)'; this.style.transform='translateY(-2px)'"
251
  onmouseout="this.style.background='rgba(255,255,255,0.15)'; this.style.transform='translateY(0px)'">
252
- Right-click β†’ "Open in new tab"
253
  </a>
254
 
255
- <div style="background: rgba(0,0,0,0.1); padding: 15px; border-radius: 8px; margin-top: 15px;">
256
- <p style="margin: 5px 0; color: rgba(255,255,255,0.9); font-size: 14px; font-weight: bold;">
257
- πŸ“‹ Copy this URL:
258
- </p>
259
- <div style="background: rgba(255,255,255,0.9); color: #333; padding: 12px; border-radius: 5px;
260
- font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all;
261
- border: 1px solid rgba(255,255,255,0.5); margin-top: 8px;"
262
- onclick="navigator.clipboard.writeText('{redirect_url}').then(() => {{
263
- this.style.background='#d4edda';
264
- this.innerHTML = 'βœ… URL Copied! Paste in new browser tab';
265
- setTimeout(() => {{
266
- this.style.background='rgba(255,255,255,0.9)';
267
- this.innerHTML = '{redirect_url}';
268
- }}, 2000);
269
- }}).catch(() => {{
270
- this.innerHTML = 'Click failed - please manually select and copy';
271
- }})"
272
- style="cursor: pointer; user-select: all;">
273
- {redirect_url}
274
- </div>
275
- <p style="margin: 8px 0 0 0; color: rgba(255,255,255,0.8); font-size: 12px;">
276
- πŸ’‘ Click the URL above to copy it automatically
277
- </p>
278
  </div>
 
 
 
279
  </div>
280
 
281
  <div style="text-align: center; padding: 20px; background: #f1f3f4; border-radius: 8px; margin-top: 20px;">
@@ -289,7 +405,7 @@ def redirect_to_space(redirect_url, selected_space, status_display):
289
  return gr.HTML(redirect_html)
290
 
291
  def load_balance_user(student_id):
292
- """Main load balancing function"""
293
  print(f"[load_balance_user] Starting load balancing for student ID: {student_id}")
294
 
295
  # Initialize connection to data storage
@@ -303,11 +419,11 @@ def load_balance_user(student_id):
303
  if not available_spaces:
304
  raise gr.Error("🚫 No student learning assistant spaces found in the system. Please contact your instructor.", duration=8)
305
 
306
- # Analyze space activity
307
  space_activity = analyze_space_activity(available_spaces, LOCAL_DATA_DIR)
308
 
309
- # Select space with boundary checking
310
- return select_space_with_boundary_check(space_activity, student_id)
311
 
312
  def get_url_params(request: gr.Request):
313
  """Extract URL parameters from request"""
@@ -315,7 +431,7 @@ def get_url_params(request: gr.Request):
315
  query_params = dict(request.query_params)
316
  check_id = query_params.get('check', None)
317
  if check_id:
318
- return f"Load Distributor", check_id
319
  else:
320
  return "Load Distributor", None
321
  return "Load Distributor", None
 
1
  import gradio as gr
2
  import os
3
  import csv
4
+ from datetime import datetime, timedelta
5
  from huggingface_hub import Repository
6
 
7
  # Configuration
 
9
  DATA_BRANCH_NAME = "data_branch"
10
  LOCAL_DATA_DIR = "temp_data_storage"
11
  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)
 
119
  print(f"[get_last_activity_time] Traceback: {traceback.format_exc()}")
120
  return datetime.min
121
 
122
+ def read_allocation_records(repo_dir):
123
+ """Read and parse allocation records, returning only non-expired allocations"""
124
+ allocation_file = os.path.join(repo_dir, ALLOCATION_RECORD_FILE)
125
+ current_time = datetime.now()
126
+ active_allocations = {}
127
+
128
+ try:
129
+ if not os.path.exists(allocation_file):
130
+ print(f"[read_allocation_records] Allocation file not found, creating new one")
131
+ # Create empty allocation file with header
132
+ with open(allocation_file, 'w', newline='', encoding='utf-8') as f:
133
+ writer = csv.writer(f)
134
+ writer.writerow(['space_name', 'student_id', 'allocated_time', 'expires_at'])
135
+ return active_allocations
136
+
137
+ with open(allocation_file, 'r', encoding='utf-8') as f:
138
+ csv_reader = csv.DictReader(f)
139
+
140
+ for row in csv_reader:
141
+ try:
142
+ expires_at = datetime.strptime(row['expires_at'], '%Y-%m-%d %H:%M:%S')
143
+ allocated_time = datetime.strptime(row['allocated_time'], '%Y-%m-%d %H:%M:%S')
144
+
145
+ # Only keep non-expired allocations
146
+ if expires_at > current_time:
147
+ active_allocations[row['space_name']] = {
148
+ 'student_id': row['student_id'],
149
+ 'allocated_time': allocated_time,
150
+ 'expires_at': expires_at
151
+ }
152
+ print(f"[read_allocation_records] Active allocation: {row['space_name']} -> {row['student_id']}")
153
+ else:
154
+ print(f"[read_allocation_records] Expired allocation: {row['space_name']} (expired at {expires_at})")
155
+
156
+ except ValueError as ve:
157
+ print(f"[read_allocation_records] Error parsing row {row}: {ve}")
158
+ continue
159
+
160
+ except Exception as e:
161
+ print(f"[read_allocation_records] Error reading allocation records: {e}")
162
+ return {}
163
+
164
+ print(f"[read_allocation_records] Found {len(active_allocations)} active allocations")
165
+ return active_allocations
166
+
167
+ def write_allocation_record(space_name, student_id, repo_dir):
168
+ """Write a new allocation record to the file"""
169
+ allocation_file = os.path.join(repo_dir, ALLOCATION_RECORD_FILE)
170
+ current_time = datetime.now()
171
+ expires_at = current_time + timedelta(minutes=ALLOCATION_LOCK_DURATION)
172
+
173
+ try:
174
+ # Read existing records
175
+ existing_records = []
176
+ current_time_check = datetime.now()
177
+
178
+ if os.path.exists(allocation_file):
179
+ with open(allocation_file, 'r', encoding='utf-8') as f:
180
+ csv_reader = csv.DictReader(f)
181
+ for row in csv_reader:
182
+ try:
183
+ row_expires_at = datetime.strptime(row['expires_at'], '%Y-%m-%d %H:%M:%S')
184
+ # Only keep non-expired records
185
+ if row_expires_at > current_time_check:
186
+ existing_records.append(row)
187
+ except ValueError:
188
+ continue
189
+
190
+ # Add new allocation record
191
+ new_record = {
192
+ 'space_name': space_name,
193
+ 'student_id': student_id,
194
+ 'allocated_time': current_time.strftime('%Y-%m-%d %H:%M:%S'),
195
+ 'expires_at': expires_at.strftime('%Y-%m-%d %H:%M:%S')
196
+ }
197
+ existing_records.append(new_record)
198
+
199
+ # Write all records back to file
200
+ with open(allocation_file, 'w', newline='', encoding='utf-8') as f:
201
+ fieldnames = ['space_name', 'student_id', 'allocated_time', 'expires_at']
202
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
203
+ writer.writeheader()
204
+ writer.writerows(existing_records)
205
+
206
+ print(f"[write_allocation_record] Recorded allocation: {space_name} -> {student_id} (expires at {expires_at})")
207
+ return True
208
+
209
+ except Exception as e:
210
+ print(f"[write_allocation_record] Error writing allocation record: {e}")
211
+ return False
212
+
213
+ def commit_allocation_record(repo, space_name, student_id):
214
+ """Commit the allocation record to Git"""
215
+ try:
216
+ repo.git_add(ALLOCATION_RECORD_FILE)
217
+ commit_message = f"Allocate {space_name} to student {student_id}"
218
+ repo.git_commit(commit_message)
219
+ repo.git_push()
220
+ print(f"[commit_allocation_record] Successfully committed allocation record")
221
+ return True
222
+ except Exception as e:
223
+ print(f"[commit_allocation_record] Error committing allocation record: {e}")
224
+ return False
225
+
226
  def analyze_space_activity(available_spaces, repo_dir):
227
  """Analyze activity status of all spaces and return sorted list"""
228
  space_activity = []
229
  current_time = datetime.now()
230
 
231
+ # Read allocation records to filter out recently allocated spaces
232
+ active_allocations = read_allocation_records(repo_dir)
233
+
234
  print(f"[analyze_space_activity] Analyzing {len(available_spaces)} spaces...")
235
 
236
  for space_name in available_spaces:
 
247
  status = f"Idle for {idle_minutes:.1f} minutes"
248
  last_activity_str = last_activity.strftime('%Y-%m-%d %H:%M:%S')
249
 
250
+ # Check if space is recently allocated
251
+ is_recently_allocated = space_name in active_allocations
252
+ if is_recently_allocated:
253
+ alloc_info = active_allocations[space_name]
254
+ minutes_until_free = (alloc_info['expires_at'] - current_time).total_seconds() / 60
255
+ status += f" (Recently allocated, free in {minutes_until_free:.1f} min)"
256
+
257
  space_activity.append({
258
  'space_name': space_name,
259
  'last_activity': last_activity,
260
  'last_activity_str': last_activity_str,
261
  'idle_minutes': idle_minutes,
262
+ 'status': status,
263
+ 'is_recently_allocated': is_recently_allocated
264
  })
265
 
266
  print(f"[analyze_space_activity] {space_name}: {status}")
 
281
 
282
  return status_display
283
 
284
+ def select_space_with_boundary_check(space_activity, student_id, repo):
285
+ """Select space with strict boundary conditions and allocation recording"""
 
 
 
 
286
 
287
+ # Filter out recently allocated spaces
288
+ available_spaces = [s for s in space_activity
289
+ if s['idle_minutes'] >= MIN_IDLE_MINUTES and not s['is_recently_allocated']]
290
 
291
  if not available_spaces:
292
+ # Check what's preventing allocation
293
+ idle_spaces = [s for s in space_activity if s['idle_minutes'] >= MIN_IDLE_MINUTES]
294
+ recently_allocated_spaces = [s for s in space_activity if s['is_recently_allocated']]
295
 
296
+ if idle_spaces and not recently_allocated_spaces:
297
+ # All spaces are busy (used within the last 30 minutes)
298
+ most_idle = space_activity[0] if space_activity else None
 
 
299
 
300
+ if most_idle:
301
+ if most_idle['idle_minutes'] == float('inf'):
302
+ idle_info = "Never used"
303
+ else:
304
+ idle_info = f"{most_idle['idle_minutes']:.1f} minutes ago"
305
+
306
+ error_msg = (
307
+ f"🚫 **All learning assistants are currently busy**\n\n"
308
+ f"All spaces have been used within the last {MIN_IDLE_MINUTES} minutes.\n\n"
309
+ f"**Current Status:**\n"
310
+ f"β€’ Most idle space: {most_idle['space_name']}\n"
311
+ f"β€’ Last used: {idle_info}\n\n"
312
+ f"**Please try again in a few minutes.**"
313
+ )
314
+ else:
315
+ error_msg = "🚫 No learning assistant spaces are available at the moment."
316
+ else:
317
+ # Spaces are recently allocated
318
  error_msg = (
319
+ f"🚫 **All learning assistants are currently busy or recently allocated**\n\n"
320
+ f"Some spaces are being assigned to other students right now.\n\n"
321
+ f"**Please try again in 1-2 minutes.**"
 
 
 
322
  )
 
 
323
 
324
+ print(f"[select_space_with_boundary_check] No available spaces: {error_msg}")
325
  raise gr.Error(error_msg, duration=10)
326
 
327
  # Select the most idle space
328
  selected_space = available_spaces[0]
329
  print(f"[select_space_with_boundary_check] Selected space: {selected_space['space_name']}")
330
 
331
+ # Record the allocation BEFORE building the response
332
+ write_success = write_allocation_record(selected_space['space_name'], student_id, repo.local_dir)
333
+ if write_success:
334
+ commit_allocation_record(repo, selected_space['space_name'], student_id)
335
+ else:
336
+ print(f"[select_space_with_boundary_check] Warning: Failed to record allocation")
337
+
338
+ # Create status display
339
+ status_display = create_status_display(space_activity)
340
+
341
  # Build redirect URL
342
  redirect_url = f"https://huggingface.co/spaces/CIV3283/{selected_space['space_name']}/?check={student_id}"
343
 
 
351
  else:
352
  idle_info = f"{selected_space['idle_minutes']:.1f} minutes"
353
 
354
+ # Modified HTML structure - Access section first, then analysis
355
  redirect_html = f"""
356
  <div style="max-width: 900px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
357
  <div style="text-align: center; margin-bottom: 30px; padding: 25px; background: linear-gradient(135deg, #28a745, #20c997); color: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
 
362
  </p>
363
  </div>
364
 
 
 
 
 
 
 
 
 
 
 
365
  <div style="text-align: center; margin-bottom: 30px; padding: 25px; background: linear-gradient(135deg, #2196f3, #1976d2); color: white; border-radius: 12px; box-shadow: 0 4px 6px rgba(33,150,243,0.3);">
366
  <h2 style="margin-top: 0; color: white;">πŸš€ Access Your Learning Assistant</h2>
367
  <p style="color: rgba(255,255,255,0.9); font-size: 16px; margin-bottom: 25px;">
368
+ Click the button below to access your assigned learning assistant.
369
  </p>
370
 
371
  <a href="{redirect_url}"
 
376
  transition: all 0.3s ease; margin-bottom: 20px;"
377
  onmouseover="this.style.background='rgba(255,255,255,0.25)'; this.style.transform='translateY(-2px)'"
378
  onmouseout="this.style.background='rgba(255,255,255,0.15)'; this.style.transform='translateY(0px)'">
379
+ ➀ Open Learning Assistant
380
  </a>
381
 
382
+ <p style="margin: 15px 0 0 0; color: rgba(255,255,255,0.8); font-size: 14px;">
383
+ πŸ’‘ Right-click the button above and select "Open in new tab"
384
+ </p>
385
+ </div>
386
+
387
+ <div style="background: #f8f9fa; padding: 25px; border-radius: 12px; margin-bottom: 25px; border-left: 4px solid #28a745;">
388
+ <h3 style="margin-top: 0; color: #333;">πŸ“Š Space Selection Analysis</h3>
389
+ <div style="background: white; padding: 20px; border-radius: 8px; font-size: 14px; line-height: 1.8; border: 1px solid #e9ecef;">
390
+ {status_display}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  </div>
392
+ <p style="margin-bottom: 0; color: #666; font-size: 14px; margin-top: 15px;">
393
+ <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.
394
+ </p>
395
  </div>
396
 
397
  <div style="text-align: center; padding: 20px; background: #f1f3f4; border-radius: 8px; margin-top: 20px;">
 
405
  return gr.HTML(redirect_html)
406
 
407
  def load_balance_user(student_id):
408
+ """Main load balancing function with allocation recording"""
409
  print(f"[load_balance_user] Starting load balancing for student ID: {student_id}")
410
 
411
  # Initialize connection to data storage
 
419
  if not available_spaces:
420
  raise gr.Error("🚫 No student learning assistant spaces found in the system. Please contact your instructor.", duration=8)
421
 
422
+ # Analyze space activity (including allocation records)
423
  space_activity = analyze_space_activity(available_spaces, LOCAL_DATA_DIR)
424
 
425
+ # Select space with boundary checking and allocation recording
426
+ return select_space_with_boundary_check(space_activity, student_id, repo)
427
 
428
  def get_url_params(request: gr.Request):
429
  """Extract URL parameters from request"""
 
431
  query_params = dict(request.query_params)
432
  check_id = query_params.get('check', None)
433
  if check_id:
434
+ return f"Load Distributor - Student {check_id}", check_id
435
  else:
436
  return "Load Distributor", None
437
  return "Load Distributor", None