linxinhua commited on
Commit
11f6214
Β·
verified Β·
1 Parent(s): 3c174eb

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +342 -0
app.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
8
+ DATA_STORAGE_REPO = "CIV3283/Data_Storage"
9
+ DATA_BRANCH_NAME = "data_branch"
10
+ LOCAL_DATA_DIR = "temp_data_storage"
11
+ MIN_IDLE_MINUTES = 30 # Minimum idle time required for space assignment
12
+
13
+ # Environment variables
14
+ HF_HUB_TOKEN = os.environ.get("HF_HUB_TOKEN", None)
15
+ if HF_HUB_TOKEN is None:
16
+ raise ValueError("Set HF_HUB_TOKEN in Space Settings -> Secrets")
17
+
18
+ def init_data_storage_connection():
19
+ """Initialize connection to centralized data storage repository"""
20
+ try:
21
+ repo = Repository(
22
+ local_dir=LOCAL_DATA_DIR,
23
+ clone_from=DATA_STORAGE_REPO,
24
+ revision=DATA_BRANCH_NAME,
25
+ repo_type="space",
26
+ use_auth_token=HF_HUB_TOKEN
27
+ )
28
+
29
+ print(f"[init_data_storage_connection] Pulling latest data from {DATA_STORAGE_REPO}...")
30
+ repo.git_pull(rebase=True)
31
+
32
+ print(f"[init_data_storage_connection] Successfully connected to data storage")
33
+ print(f"[init_data_storage_connection] Local directory: {LOCAL_DATA_DIR}")
34
+ print(f"[init_data_storage_connection] Branch: {DATA_BRANCH_NAME}")
35
+
36
+ return repo
37
+
38
+ except Exception as e:
39
+ print(f"[init_data_storage_connection] Error: {e}")
40
+ return None
41
+
42
+ def get_available_spaces(repo_dir):
43
+ """Dynamically get space list from CIV3283/Data_Storage"""
44
+ available_spaces = set()
45
+
46
+ try:
47
+ # Scan all *_query_log.csv files
48
+ for filename in os.listdir(repo_dir):
49
+ if filename.endswith('_query_log.csv') and '_Student_' in filename:
50
+ # Extract space name from filename: CIV3283_Student_01_query_log.csv β†’ CIV3283_Student_01
51
+ space_name = filename.replace('_query_log.csv', '')
52
+ available_spaces.add(space_name)
53
+
54
+ spaces_list = sorted(list(available_spaces))
55
+ print(f"[get_available_spaces] Found {len(spaces_list)} student spaces: {spaces_list}")
56
+ return spaces_list
57
+
58
+ except Exception as e:
59
+ print(f"[get_available_spaces] Error: {e}")
60
+ return []
61
+
62
+ def get_last_activity_time(csv_file_path):
63
+ """Get the timestamp of the last activity from query log file"""
64
+ try:
65
+ if not os.path.exists(csv_file_path):
66
+ print(f"[get_last_activity_time] File not found: {csv_file_path}")
67
+ return datetime.min # Never used
68
+
69
+ with open(csv_file_path, 'r', encoding='utf-8') as f:
70
+ lines = f.readlines()
71
+
72
+ if len(lines) <= 1: # Only header or empty file
73
+ print(f"[get_last_activity_time] No data in file: {csv_file_path}")
74
+ return datetime.min # Never used
75
+
76
+ # Get last line and parse timestamp (3rd column, index 2)
77
+ last_line = lines[-1].strip()
78
+ if not last_line:
79
+ return datetime.min
80
+
81
+ # Parse CSV line
82
+ csv_reader = csv.reader([last_line])
83
+ row = next(csv_reader)
84
+
85
+ if len(row) >= 3:
86
+ timestamp_str = row[2].strip() # timestamp column
87
+ try:
88
+ return datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
89
+ except ValueError as ve:
90
+ print(f"[get_last_activity_time] Date parsing error: {ve}")
91
+ return datetime.min
92
+ else:
93
+ print(f"[get_last_activity_time] Invalid CSV format in {csv_file_path}")
94
+ return datetime.min
95
+
96
+ except Exception as e:
97
+ print(f"[get_last_activity_time] Error reading {csv_file_path}: {e}")
98
+ return datetime.min
99
+
100
+ def analyze_space_activity(available_spaces, repo_dir):
101
+ """Analyze activity status of all spaces and return sorted list"""
102
+ space_activity = []
103
+ current_time = datetime.now()
104
+
105
+ print(f"[analyze_space_activity] Analyzing {len(available_spaces)} spaces...")
106
+
107
+ for space_name in available_spaces:
108
+ csv_file = os.path.join(repo_dir, f"{space_name}_query_log.csv")
109
+ last_activity = get_last_activity_time(csv_file)
110
+
111
+ # Calculate idle time in minutes
112
+ if last_activity == datetime.min:
113
+ idle_minutes = float('inf') # Never used
114
+ status = "Never used"
115
+ last_activity_str = "Never"
116
+ else:
117
+ idle_minutes = (current_time - last_activity).total_seconds() / 60
118
+ status = f"Idle for {idle_minutes:.1f} minutes"
119
+ last_activity_str = last_activity.strftime('%Y-%m-%d %H:%M:%S')
120
+
121
+ space_activity.append({
122
+ 'space_name': space_name,
123
+ 'last_activity': last_activity,
124
+ 'last_activity_str': last_activity_str,
125
+ 'idle_minutes': idle_minutes,
126
+ 'status': status
127
+ })
128
+
129
+ print(f"[analyze_space_activity] {space_name}: {status}")
130
+
131
+ # Sort by idle time (most idle first)
132
+ space_activity.sort(key=lambda x: x['idle_minutes'], reverse=True)
133
+
134
+ return space_activity
135
+
136
+ def create_status_display(space_activity):
137
+ """Create formatted status display for all spaces"""
138
+ status_display = "πŸ“Š **Current Space Status (sorted by availability):**\n\n"
139
+
140
+ for i, space in enumerate(space_activity, 1):
141
+ status_display += f"{i}. **{space['space_name']}**\n"
142
+ status_display += f" β€’ Status: {space['status']}\n"
143
+ status_display += f" β€’ Last activity: {space['last_activity_str']}\n\n"
144
+
145
+ return status_display
146
+
147
+ def select_space_with_boundary_check(space_activity, student_id):
148
+ """Select space with strict boundary conditions"""
149
+
150
+ # Create and display current status
151
+ status_display = create_status_display(space_activity)
152
+ print(f"[select_space_with_boundary_check] Space analysis:\n{status_display}")
153
+
154
+ # Check for available spaces (idle for at least MIN_IDLE_MINUTES)
155
+ available_spaces = [s for s in space_activity if s['idle_minutes'] >= MIN_IDLE_MINUTES]
156
+
157
+ if not available_spaces:
158
+ # All spaces are busy (used within the last 30 minutes)
159
+ most_idle = space_activity[0] if space_activity else None
160
+
161
+ if most_idle:
162
+ if most_idle['idle_minutes'] == float('inf'):
163
+ idle_info = "Never used"
164
+ else:
165
+ idle_info = f"{most_idle['idle_minutes']:.1f} minutes ago"
166
+
167
+ error_msg = (
168
+ f"🚫 **All learning assistants are currently busy**\n\n"
169
+ f"All spaces have been used within the last {MIN_IDLE_MINUTES} minutes.\n\n"
170
+ f"**Current Status:**\n"
171
+ f"β€’ Most idle space: {most_idle['space_name']}\n"
172
+ f"β€’ Last used: {idle_info}\n\n"
173
+ f"**Please try again in a few minutes.**"
174
+ )
175
+ else:
176
+ error_msg = "🚫 No learning assistant spaces are available at the moment."
177
+
178
+ print(f"[select_space_with_boundary_check] All spaces busy: {error_msg}")
179
+ raise gr.Error(error_msg, duration=10)
180
+
181
+ # Select the most idle space
182
+ selected_space = available_spaces[0]
183
+ print(f"[select_space_with_boundary_check] Selected space: {selected_space['space_name']}")
184
+
185
+ # Build redirect URL
186
+ redirect_url = f"https://huggingface.co/spaces/{selected_space['space_name']}/?check={student_id}"
187
+
188
+ return redirect_to_space(redirect_url, selected_space, status_display)
189
+
190
+ def redirect_to_space(redirect_url, selected_space, status_display):
191
+ """Execute redirect with detailed information display"""
192
+
193
+ if selected_space['idle_minutes'] == float('inf'):
194
+ idle_info = "Never used (completely fresh)"
195
+ else:
196
+ idle_info = f"{selected_space['idle_minutes']:.1f} minutes"
197
+
198
+ redirect_html = f"""
199
+ <div style="max-width: 900px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
200
+ <div style="text-align: center; margin-bottom: 30px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 12px;">
201
+ <h1 style="margin: 0 0 10px 0;">🎯 Learning Assistant Assigned</h1>
202
+ <h2 style="margin: 0; font-weight: normal; opacity: 0.9;">{selected_space['space_name']}</h2>
203
+ <p style="margin: 10px 0 0 0; font-size: 16px;">
204
+ ✨ This space was idle for: <strong>{idle_info}</strong>
205
+ </p>
206
+ </div>
207
+
208
+ <div style="background: #f8f9fa; padding: 25px; border-radius: 12px; margin-bottom: 25px; border-left: 4px solid #007bff;">
209
+ <h3 style="margin-top: 0; color: #333;">πŸ“Š Space Selection Analysis</h3>
210
+ <div style="background: white; padding: 20px; border-radius: 8px; font-family: 'Courier New', monospace; font-size: 14px; line-height: 1.6; overflow-x: auto; border: 1px solid #e9ecef;">
211
+ {status_display}
212
+ </div>
213
+ <p style="margin-bottom: 0; color: #666; font-size: 14px; margin-top: 15px;">
214
+ <strong>Selection Algorithm:</strong> Spaces idle for β‰₯{MIN_IDLE_MINUTES} minutes are eligible. The most idle space is automatically selected for optimal load distribution.
215
+ </p>
216
+ </div>
217
+
218
+ <div style="text-align: center; background: linear-gradient(135deg, #28a745, #20c997); color: white; padding: 25px; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
219
+ <h3 style="margin-top: 0;">πŸ”„ Redirecting to Your Learning Assistant</h3>
220
+ <p style="margin: 15px 0;">You will be automatically redirected in <span id="countdown">5</span> seconds.</p>
221
+
222
+ <div style="margin: 25px 0;">
223
+ <a href="{redirect_url}"
224
+ style="display: inline-block; background: rgba(255,255,255,0.2); color: white; padding: 12px 24px;
225
+ text-decoration: none; border-radius: 25px; font-weight: bold; border: 2px solid rgba(255,255,255,0.3);
226
+ transition: all 0.3s ease;"
227
+ onmouseover="this.style.background='rgba(255,255,255,0.3)'"
228
+ onmouseout="this.style.background='rgba(255,255,255,0.2)'">
229
+ Click Here if Not Redirected Automatically
230
+ </a>
231
+ </div>
232
+
233
+ <details style="margin-top: 20px; text-align: left;">
234
+ <summary style="cursor: pointer; color: rgba(255,255,255,0.9);">πŸ”— View Target URL</summary>
235
+ <p style="margin: 10px 0 0 0; padding: 10px; background: rgba(0,0,0,0.1); border-radius: 5px; word-break: break-all; font-family: monospace; font-size: 12px;">
236
+ {redirect_url}
237
+ </p>
238
+ </details>
239
+ </div>
240
+ </div>
241
+
242
+ <script>
243
+ let countdown = 5;
244
+ const countdownElement = document.getElementById('countdown');
245
+
246
+ const timer = setInterval(function() {{
247
+ countdown--;
248
+ if (countdownElement) {{
249
+ countdownElement.textContent = countdown;
250
+ }}
251
+
252
+ if (countdown <= 0) {{
253
+ clearInterval(timer);
254
+ window.location.href = "{redirect_url}";
255
+ }}
256
+ }}, 1000);
257
+ </script>
258
+ """
259
+
260
+ return gr.HTML(redirect_html)
261
+
262
+ def load_balance_user(student_id):
263
+ """Main load balancing function"""
264
+ print(f"[load_balance_user] Starting load balancing for student ID: {student_id}")
265
+
266
+ # Initialize connection to data storage
267
+ repo = init_data_storage_connection()
268
+ if not repo:
269
+ raise gr.Error("🚫 Unable to connect to the data storage system. Please try again later.", duration=8)
270
+
271
+ # Get available spaces dynamically
272
+ available_spaces = get_available_spaces(LOCAL_DATA_DIR)
273
+
274
+ if not available_spaces:
275
+ raise gr.Error("🚫 No student learning assistant spaces found in the system. Please contact your instructor.", duration=8)
276
+
277
+ # Analyze space activity
278
+ space_activity = analyze_space_activity(available_spaces, LOCAL_DATA_DIR)
279
+
280
+ # Select space with boundary checking
281
+ return select_space_with_boundary_check(space_activity, student_id)
282
+
283
+ def get_url_params(request: gr.Request):
284
+ """Extract URL parameters from request"""
285
+ if request:
286
+ query_params = dict(request.query_params)
287
+ check_id = query_params.get('check', None)
288
+ if check_id:
289
+ return f"Load Distributor - Student {check_id}", check_id
290
+ else:
291
+ return "Load Distributor", None
292
+ return "Load Distributor", None
293
+
294
+ def handle_user_access(request: gr.Request):
295
+ """Handle user access and perform load balancing"""
296
+ title, check_id = get_url_params(request)
297
+
298
+ if not check_id:
299
+ # No student ID provided
300
+ error_html = """
301
+ <div style="max-width: 600px; margin: 50px auto; padding: 30px; text-align: center;
302
+ background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 12px;">
303
+ <h2 style="color: #856404;">⚠️ Invalid Access</h2>
304
+ <p style="color: #856404; font-size: 16px; line-height: 1.6;">
305
+ This load distributor requires a valid student ID parameter.<br><br>
306
+ <strong>Please access this system through the official link provided in Moodle.</strong>
307
+ </p>
308
+ </div>
309
+ """
310
+ return title, gr.HTML(error_html)
311
+
312
+ # Valid student ID - perform load balancing
313
+ try:
314
+ result = load_balance_user(check_id)
315
+ return title, result
316
+ except Exception as e:
317
+ # Handle any errors during load balancing
318
+ error_html = f"""
319
+ <div style="max-width: 600px; margin: 50px auto; padding: 30px; text-align: center;
320
+ background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 12px;">
321
+ <h2 style="color: #721c24;">🚫 Load Balancing Error</h2>
322
+ <p style="color: #721c24; font-size: 16px; line-height: 1.6;">
323
+ {str(e)}<br><br>
324
+ Please try again in a few moments or contact your instructor if the problem persists.
325
+ </p>
326
+ </div>
327
+ """
328
+ return title, gr.HTML(error_html)
329
+
330
+ # Create Gradio interface
331
+ with gr.Blocks(title="CIV3283 Load Distributor") as interface:
332
+ title_display = gr.Markdown("# πŸ”„ CIV3283 Learning Assistant Load Distributor", elem_id="title")
333
+ content_display = gr.HTML("")
334
+
335
+ # Initialize on page load
336
+ interface.load(
337
+ fn=handle_user_access,
338
+ outputs=[title_display, content_display]
339
+ )
340
+
341
+ if __name__ == "__main__":
342
+ interface.launch()