linxinhua commited on
Commit
b48f33d
·
verified ·
1 Parent(s): fa76df4

Update app.py via admin tool

Browse files
Files changed (1) hide show
  1. app.py +547 -1087
app.py CHANGED
@@ -1,835 +1,393 @@
1
  import gradio as gr
2
- import os
3
  import csv
 
 
4
  from datetime import datetime, timedelta
5
  from huggingface_hub import Repository
6
- import threading
7
- import time
8
- import json
9
 
10
-
11
- # Configuration
12
- DATA_STORAGE_REPO = "CIV3283/Data_Storage"
 
 
 
 
 
 
13
  DATA_BRANCH_NAME = "data_branch"
14
- LOCAL_DATA_DIR = "temp_data_storage"
15
- MIN_IDLE_MINUTES = 10 # Minimum idle time required for space assignment
16
- ALLOCATION_LOCK_DURATION = 5 # Lock duration in minutes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- # 缓存配置 - 全部改为内存存储
19
- CACHE_UPDATE_INTERVAL = 600 # 10分钟 = 600秒
20
- DATA_SYNC_INTERVAL = 600 # 数据同步间隔,10分钟 = 600秒
21
- CACHE_LOCK = threading.Lock()
22
- ALLOCATION_LOCK = threading.Lock()
23
 
24
- # 全局内存存储
25
- class MemoryAllocationStore:
26
- """内存中的分配记录存储"""
27
- def __init__(self):
28
- self._allocations = {} # {space_name: {'student_id': str, 'allocated_time': datetime, 'expires_at': datetime}}
29
- self._lock = threading.Lock()
30
- self._cleanup_interval = 60 # 每分钟清理一次过期记录
 
 
 
 
 
 
31
 
32
- # 启动后台清理线程
33
- self._start_cleanup_thread()
34
-
35
- def _start_cleanup_thread(self):
36
- """启动后台清理过期分配记录的线程"""
37
- def cleanup_worker():
38
- while True:
39
- time.sleep(self._cleanup_interval)
40
- self._cleanup_expired_allocations()
41
 
42
- cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
43
- cleanup_thread.start()
44
- print("[MemoryAllocationStore] Background cleanup thread started")
45
-
46
- def _cleanup_expired_allocations(self):
47
- """清理过期的分配记录"""
48
- with self._lock:
49
- current_time = datetime.now()
50
- expired_spaces = []
51
-
52
- for space_name, alloc_info in self._allocations.items():
53
- if alloc_info['expires_at'] <= current_time:
54
- expired_spaces.append(space_name)
55
-
56
- for space_name in expired_spaces:
57
- del self._allocations[space_name]
58
- if expired_spaces: # 只在有过期记录时打印
59
- print(f"[MemoryAllocationStore] Cleaned up expired allocation: {space_name}")
60
-
61
- def add_allocation(self, space_name, student_id):
62
- """添加新的分配记录"""
63
- with self._lock:
64
- current_time = datetime.now()
65
- expires_at = current_time + timedelta(minutes=ALLOCATION_LOCK_DURATION)
66
-
67
- self._allocations[space_name] = {
68
- 'student_id': student_id,
69
- 'allocated_time': current_time,
70
- 'expires_at': expires_at
71
- }
72
-
73
- print(f"[MemoryAllocationStore] Added allocation: {space_name} -> {student_id} (expires at {expires_at.strftime('%H:%M:%S')})")
74
-
75
- def get_active_allocations(self):
76
- """��取所有有效的分配记录"""
77
- with self._lock:
78
- current_time = datetime.now()
79
- active_allocations = {}
80
 
81
- for space_name, alloc_info in self._allocations.items():
82
- if alloc_info['expires_at'] > current_time:
83
- active_allocations[space_name] = alloc_info.copy()
 
84
 
85
- return active_allocations
86
-
87
- def is_allocated(self, space_name):
88
- """检查指定空间是否被分配"""
89
- with self._lock:
90
- if space_name not in self._allocations:
91
- return False, None
92
-
93
- alloc_info = self._allocations[space_name]
94
- current_time = datetime.now()
95
 
96
- if alloc_info['expires_at'] > current_time:
97
- return True, alloc_info['student_id']
98
- else:
99
- # 过期了,删除记录
100
- del self._allocations[space_name]
101
- return False, None
102
-
103
- def get_status_summary(self):
104
- """获取分配状态摘要"""
105
- with self._lock:
106
- current_time = datetime.now()
107
- active_count = 0
108
- summary = []
109
-
110
- for space_name, alloc_info in self._allocations.items():
111
- if alloc_info['expires_at'] > current_time:
112
- active_count += 1
113
- remaining_minutes = (alloc_info['expires_at'] - current_time).total_seconds() / 60
114
- summary.append(f"{space_name} -> {alloc_info['student_id']} ({remaining_minutes:.1f}min left)")
115
-
116
- return {
117
- 'active_count': active_count,
118
- 'total_stored': len(self._allocations),
119
- 'summary': summary
120
- }
121
-
122
- # 全局数据管理器
123
- class DataManager:
124
- def __init__(self):
125
- self.repo = None
126
- self.available_spaces = []
127
- self.last_data_sync = None
128
- self._sync_lock = threading.Lock()
129
- self._sync_thread = None
130
- self._stop_event = threading.Event()
131
- self.initialized = False
132
-
133
- def initialize(self):
134
- """初始化数据管理器 - 只在启动时调用一次"""
135
- if self.initialized:
136
- return True
137
-
138
- try:
139
- print("[DataManager] Initializing data manager...")
140
 
141
- # 初始化仓库连接
142
- self.repo = Repository(
143
- local_dir=LOCAL_DATA_DIR,
144
- clone_from=DATA_STORAGE_REPO,
145
- revision=DATA_BRANCH_NAME,
146
- repo_type="space",
147
- use_auth_token=os.environ.get("HF_HUB_TOKEN")
148
- )
149
 
150
- # 配置git用户
151
- self.repo.git_config_username_and_email("git_user", f"load_distributor")
152
- self.repo.git_config_username_and_email("git_email", f"loaddistributor@takeiteasy.space")
153
 
154
- # 初始数据同步
155
- self._sync_data()
 
 
156
 
157
- # 启动后台同步线程
158
- self._start_background_sync()
159
-
160
- self.initialized = True
161
- print("[DataManager] Data manager initialized successfully")
162
- return True
163
-
164
- except Exception as e:
165
- print(f"[DataManager] Initialization failed: {e}")
166
- return False
167
-
168
- def _start_background_sync(self):
169
- """启动后台数据同步线程"""
170
- if self._sync_thread and self._sync_thread.is_alive():
171
- return
172
-
173
- self._stop_event.clear()
174
- self._sync_thread = threading.Thread(target=self._background_sync_worker, daemon=True)
175
- self._sync_thread.start()
176
- print("[DataManager] Background sync thread started")
177
-
178
- def _background_sync_worker(self):
179
- """后台同步工作线程"""
180
- while not self._stop_event.is_set():
181
  try:
182
- # 等待指定间隔或停止信号
183
- if self._stop_event.wait(timeout=DATA_SYNC_INTERVAL):
184
- break
185
 
186
- print("[DataManager] Starting scheduled data sync...")
187
- self._sync_data()
188
- print("[DataManager] Scheduled data sync completed")
189
 
190
- except Exception as e:
191
- print(f"[DataManager] Error in background sync: {e}")
192
- if not self._stop_event.wait(timeout=60): # 1分钟后重试
193
- continue
194
-
195
- def _sync_data(self):
196
- """同步数据 - 拉取最新数据并更新可用空间列表"""
197
- with self._sync_lock:
198
- try:
199
- start_time = time.time()
200
- print("[DataManager] Syncing data from remote repository...")
201
-
202
- # 拉取最新数据
203
- self.repo.git_pull(rebase=True)
204
-
205
- # 更新可用空间列表
206
- self.available_spaces = self._get_available_spaces()
207
- self.last_data_sync = datetime.now()
208
 
209
- elapsed_time = time.time() - start_time
210
- print(f"[DataManager] Data sync completed in {elapsed_time:.2f}s, found {len(self.available_spaces)} spaces")
 
211
 
212
- except Exception as e:
213
- print(f"[DataManager] Error syncing data: {e}")
214
-
215
- def _get_available_spaces(self):
216
- """获取可用空间列表"""
217
- available_spaces = set()
218
- try:
219
- for filename in os.listdir(LOCAL_DATA_DIR):
220
- if filename.endswith('_query_log.csv') and '_Student_' in filename:
221
- space_name = filename.replace('_query_log.csv', '')
222
- available_spaces.add(space_name)
223
- return sorted(list(available_spaces))
224
- except Exception as e:
225
- print(f"[DataManager] Error getting available spaces: {e}")
226
- return []
227
 
228
- def get_available_spaces(self):
229
- """获取可用空间列表 - 不触发数据同步"""
230
- if not self.initialized:
231
- raise Exception("DataManager not initialized")
232
- return self.available_spaces.copy()
233
-
234
- def get_repo(self):
235
- """获取仓库对象"""
236
- if not self.initialized:
237
- raise Exception("DataManager not initialized")
238
- return self.repo
239
-
240
- def get_repo_dir(self):
241
- """获取仓库本地目录"""
242
- if not self.initialized:
243
- raise Exception("DataManager not initialized")
244
- return LOCAL_DATA_DIR
245
-
246
- def stop(self):
247
- """停止后台同步"""
248
- if self._sync_thread:
249
- self._stop_event.set()
250
- print("[DataManager] Background sync thread stopping...")
251
 
252
- class SpaceActivityCache:
253
- """内存中的空间活动缓存"""
254
- def __init__(self, data_manager):
255
- self.data_manager = data_manager
256
- self._cache_data = {}
257
- self._last_update = None
258
- self._update_thread = None
259
- self._stop_event = threading.Event()
260
-
261
- # 启动时立即更新一次缓存
262
- self._update_cache()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
- # 启动后台更新线程
265
- self.start_background_updates()
266
-
267
- def start_background_updates(self):
268
- """启动后台缓存更新线程"""
269
- if self._update_thread and self._update_thread.is_alive():
270
- return
271
 
272
- self._stop_event.clear()
273
- self._update_thread = threading.Thread(target=self._background_update_worker, daemon=True)
274
- self._update_thread.start()
275
- print("[SpaceActivityCache] Background update thread started")
 
 
 
 
 
 
 
276
 
277
- def stop_background_updates(self):
278
- """停止后台更新线程"""
279
- if self._update_thread:
280
- self._stop_event.set()
281
- print("[SpaceActivityCache] Background update thread stopping...")
282
 
283
- def _background_update_worker(self):
284
- """后台更新工作线程"""
285
- while not self._stop_event.is_set():
286
- try:
287
- # 等待指定间隔或停止信号
288
- if self._stop_event.wait(timeout=CACHE_UPDATE_INTERVAL):
289
- break
290
-
291
- print("[SpaceActivityCache] Starting scheduled cache update...")
292
- self._update_cache()
293
- print("[SpaceActivityCache] Scheduled cache update completed")
294
-
295
- except Exception as e:
296
- print(f"[SpaceActivityCache] Error in background update: {e}")
297
- if not self._stop_event.wait(timeout=60): # 1分钟后重试
298
- continue
299
-
300
- def _update_cache(self):
301
- """更新缓存数据"""
302
  try:
303
- print("[SpaceActivityCache] Updating activity cache...")
304
- start_time = time.time()
305
-
306
- # 从数据管理器获取可用空间(不触发数据同步)
307
- available_spaces = self.data_manager.get_available_spaces()
308
- repo_dir = self.data_manager.get_repo_dir()
309
 
310
- new_cache_data = {
311
- 'last_updated': datetime.now().isoformat(),
312
- 'update_interval_seconds': CACHE_UPDATE_INTERVAL,
313
- 'spaces': {}
314
- }
315
 
316
- # 为每个空间读取最后活动时间
317
- for space_name in available_spaces:
318
- csv_file = os.path.join(repo_dir, f"{space_name}_query_log.csv")
319
- last_activity, status = self._get_last_activity_from_file(csv_file)
320
-
321
- new_cache_data['spaces'][space_name] = {
322
- 'last_activity': last_activity.isoformat() if last_activity != datetime.min else None,
323
- 'status': status,
324
- 'cache_update_time': datetime.now().isoformat()
325
- }
326
-
327
- print(f"[SpaceActivityCache] {space_name}: {status}")
328
 
329
- # 线程安全地更新缓存
330
- with CACHE_LOCK:
331
- self._cache_data = new_cache_data
332
- self._last_update = datetime.now()
333
 
334
- elapsed_time = time.time() - start_time
335
- print(f"[SpaceActivityCache] Cache updated in {elapsed_time:.2f}s, {len(available_spaces)} spaces processed")
 
 
336
 
337
- except Exception as e:
338
- print(f"[SpaceActivityCache] Error updating cache: {e}")
339
- import traceback
340
- print(f"[SpaceActivityCache] Traceback: {traceback.format_exc()}")
341
-
342
- def _get_last_activity_from_file(self, csv_file_path):
343
- """从文件读取最后活动时间"""
344
- try:
345
- if not os.path.exists(csv_file_path):
346
- return datetime.min, "file_not_found"
347
-
348
- # 检查文件大小
349
- file_size = os.path.getsize(csv_file_path)
350
- if file_size <= 100:
351
- return datetime.min, "empty_or_header_only"
352
-
353
- # 使用CSV reader读取最后一行
354
- with open(csv_file_path, 'r', encoding='utf-8') as f:
355
- csv_reader = csv.reader(f)
356
 
357
- # 跳过header
358
- try:
359
- header = next(csv_reader)
360
- except StopIteration:
361
- return datetime.min, "empty_file"
362
 
363
- # 读取所有行,获取最后一行
364
- rows = []
365
- try:
366
- for row in csv_reader:
367
- if row: # 跳过空行
368
- rows.append(row)
369
- except Exception as csv_error:
370
- print(f"[SpaceActivityCache] CSV parsing error for {csv_file_path}: {csv_error}")
371
- return datetime.min, "csv_parse_error"
372
-
373
- if not rows:
374
- return datetime.min, "no_data_rows"
375
-
376
- # 解析最后一行的时间戳
377
- last_row = rows[-1]
378
- if len(last_row) >= 3:
379
- timestamp_str = last_row[2].strip() # timestamp column
380
- try:
381
- parsed_time = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S')
382
- return parsed_time, f"active_last_at_{parsed_time.strftime('%Y-%m-%d_%H:%M:%S')}"
383
- except ValueError as ve:
384
- print(f"[SpaceActivityCache] Date parsing error for '{timestamp_str}': {ve}")
385
- return datetime.min, "date_parse_error"
386
- else:
387
- return datetime.min, "invalid_row_format"
388
 
389
- except Exception as e:
390
- print(f"[SpaceActivityCache] Error reading {csv_file_path}: {e}")
391
- return datetime.min, "read_error"
392
-
393
- def get_space_activity(self, space_name):
394
- """获取指定空间的活动信息"""
395
- with CACHE_LOCK:
396
- if not self._cache_data or 'spaces' not in self._cache_data:
397
- print("[SpaceActivityCache] No cache available, updating immediately...")
398
- self._update_cache()
399
 
400
- spaces_data = self._cache_data.get('spaces', {})
401
- space_info = spaces_data.get(space_name, {})
 
402
 
403
- if not space_info:
404
- return datetime.min, "not_in_cache"
 
405
 
406
- # 解析时间戳
407
- last_activity_str = space_info.get('last_activity')
408
- if last_activity_str:
409
- try:
410
- last_activity = datetime.fromisoformat(last_activity_str)
411
- except:
412
- last_activity = datetime.min
413
- else:
414
- last_activity = datetime.min
415
 
416
- status = space_info.get('status', 'unknown')
417
- return last_activity, status
418
-
419
- def get_all_spaces_activity(self):
420
- """获取所有空间的活动信息"""
421
- with CACHE_LOCK:
422
- if not self._cache_data or 'spaces' not in self._cache_data:
423
- print("[SpaceActivityCache] No cache available, updating immediately...")
424
- self._update_cache()
425
 
426
- result = {}
427
- spaces_data = self._cache_data.get('spaces', {})
 
428
 
429
- for space_name, space_info in spaces_data.items():
430
- last_activity_str = space_info.get('last_activity')
431
- if last_activity_str:
 
 
 
 
432
  try:
433
- last_activity = datetime.fromisoformat(last_activity_str)
434
- except:
435
- last_activity = datetime.min
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  else:
437
- last_activity = datetime.min
438
-
439
- result[space_name] = {
440
- 'last_activity': last_activity,
441
- 'status': space_info.get('status', 'unknown')
442
- }
443
-
444
- return result
445
-
446
- def get_cache_info(self):
447
- """获取缓存状态信息"""
448
- with CACHE_LOCK:
449
- if self._cache_data and 'last_updated' in self._cache_data:
450
- last_updated_str = self._cache_data['last_updated']
451
- try:
452
- last_updated = datetime.fromisoformat(last_updated_str)
453
- age_minutes = (datetime.now() - last_updated).total_seconds() / 60
454
- return {
455
- 'last_updated': last_updated,
456
- 'age_minutes': age_minutes,
457
- 'spaces_count': len(self._cache_data.get('spaces', {})),
458
- 'is_fresh': age_minutes < (CACHE_UPDATE_INTERVAL / 60) * 1.5
459
- }
460
- except:
461
- pass
462
-
463
- return {
464
- 'last_updated': None,
465
- 'age_minutes': float('inf'),
466
- 'spaces_count': 0,
467
- 'is_fresh': False
468
- }
469
 
470
- def force_update(self):
471
- """强制立即更新缓存"""
472
- print("[SpaceActivityCache] Force updating cache...")
473
- self._update_cache()
474
 
475
- # 新增:本地防撞车机制
476
- class LocalAllocationTracker:
477
- def __init__(self):
478
- self._recent_allocations = {} # {space_name: {'student_id': str, 'timestamp': datetime}}
479
- self._lock = threading.Lock()
480
- self._cleanup_interval = 600 # 清理间隔(秒)
481
- self._allocation_ttl = 60 # 本地分配记录的生存时间(秒)
482
 
483
- # 启动后台清理线程
484
- self._start_cleanup_thread()
485
-
486
- def _start_cleanup_thread(self):
487
- """启动后台线程定期清理过期的本地分配记录"""
488
- def cleanup_worker():
489
- while True:
490
- time.sleep(self._cleanup_interval)
491
- self._cleanup_expired_allocations()
492
 
493
- cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
494
- cleanup_thread.start()
495
- print("[LocalAllocationTracker] Background cleanup thread started")
496
-
497
- def _cleanup_expired_allocations(self):
498
- """清理过期的本地分配记录"""
499
- with self._lock:
500
- current_time = datetime.now()
501
- expired_spaces = []
502
-
503
- for space_name, alloc_info in self._recent_allocations.items():
504
- if (current_time - alloc_info['timestamp']).total_seconds() > self._allocation_ttl:
505
- expired_spaces.append(space_name)
506
-
507
- for space_name in expired_spaces:
508
- del self._recent_allocations[space_name]
509
- if expired_spaces: # 只在有过期记录时打印
510
- print(f"[LocalAllocationTracker] Cleaned up expired allocation: {space_name}")
511
-
512
- def is_recently_allocated_locally(self, space_name):
513
- """检查空间是否在本地被最近分配过"""
514
- with self._lock:
515
- if space_name not in self._recent_allocations:
516
- return False, None
517
-
518
- alloc_info = self._recent_allocations[space_name]
519
- current_time = datetime.now()
520
- elapsed_seconds = (current_time - alloc_info['timestamp']).total_seconds()
521
-
522
- if elapsed_seconds > self._allocation_ttl:
523
- # 过期了,删除记录
524
- del self._recent_allocations[space_name]
525
- print(f"[LocalAllocationTracker] Expired local allocation removed: {space_name}")
526
- return False, None
527
-
528
- print(f"[LocalAllocationTracker] Space {space_name} recently allocated locally to {alloc_info['student_id']} ({elapsed_seconds:.1f}s ago)")
529
- return True, alloc_info['student_id']
530
-
531
- def record_local_allocation(self, space_name, student_id):
532
- """记录本地分配"""
533
- with self._lock:
534
- self._recent_allocations[space_name] = {
535
- 'student_id': student_id,
536
- 'timestamp': datetime.now()
537
- }
538
- print(f"[LocalAllocationTracker] Locally recorded allocation: {space_name} -> {student_id}")
539
-
540
- def get_recent_allocations_summary(self):
541
- """获取最近本地分配的摘要(用于调试)"""
542
- with self._lock:
543
- current_time = datetime.now()
544
- summary = []
545
- for space_name, alloc_info in self._recent_allocations.items():
546
- elapsed = (current_time - alloc_info['timestamp']).total_seconds()
547
- summary.append(f"{space_name} -> {alloc_info['student_id']} ({elapsed:.1f}s ago)")
548
- return summary
549
-
550
- # 全局实例
551
- data_manager = DataManager()
552
- activity_cache = None
553
- local_tracker = LocalAllocationTracker()
554
- memory_allocation_store = MemoryAllocationStore()
555
-
556
- def init_system():
557
- """初始化整个系统 - 只在启动时调用一次"""
558
- global activity_cache
559
-
560
- print("[init_system] Initializing load distributor system...")
561
-
562
- # 初始化数据管理器
563
- if not data_manager.initialize():
564
- raise Exception("Failed to initialize data manager")
565
-
566
- # 初始化活动缓存
567
- activity_cache = SpaceActivityCache(data_manager)
568
-
569
- print("[init_system] System initialization completed")
570
-
571
- def analyze_space_activity_cached():
572
- """使用缓存的空间活动分析 - 完全使用内存数据"""
573
- global activity_cache
574
-
575
- if activity_cache is None:
576
- raise Exception("Activity cache not initialized")
577
-
578
- # 从数据管理器获取可用空间列表(不触发数据同步)
579
- available_spaces = data_manager.get_available_spaces()
580
-
581
- space_activity = []
582
- current_time = datetime.now()
583
-
584
- # 获取缓存信息
585
- cache_info = activity_cache.get_cache_info()
586
- print(f"[analyze_space_activity_cached] Using cache (age: {cache_info['age_minutes']:.1f} min, fresh: {cache_info['is_fresh']})")
587
-
588
- # 获取所有空间的缓存活动数据
589
- all_spaces_activity = activity_cache.get_all_spaces_activity()
590
-
591
- # 从内存获取分配记录 - 不再读取文件
592
- active_allocations = memory_allocation_store.get_active_allocations()
593
-
594
- print(f"[analyze_space_activity_cached] Analyzing {len(available_spaces)} spaces using cached data...")
595
- print(f"[analyze_space_activity_cached] Memory allocations: {len(active_allocations)} active")
596
-
597
- for space_name in available_spaces:
598
- # 从缓存获取活动信息
599
- cached_info = all_spaces_activity.get(space_name)
600
 
601
- if cached_info:
602
- last_activity = cached_info['last_activity']
603
- cached_status = cached_info['status']
604
- else:
605
- # 缓存中没有这个空间,可能是新空间
606
- last_activity = datetime.min
607
- cached_status = "not_in_cache"
608
 
609
- # Calculate idle time in minutes
610
- if last_activity == datetime.min or 'empty' in cached_status or 'not_found' in cached_status:
611
- idle_minutes = float('inf') # Never used
612
- status = "Never used"
613
- last_activity_str = "Never"
614
- else:
615
- idle_minutes = (current_time - last_activity).total_seconds() / 60
616
- status = f"Idle for {idle_minutes:.1f} minutes (cached)"
617
- last_activity_str = last_activity.strftime('%Y-%m-%d %H:%M:%S')
618
 
619
- # 检查内存中的分配记录
620
- is_recently_allocated_memory = space_name in active_allocations
621
- if is_recently_allocated_memory:
622
- alloc_info = active_allocations[space_name]
623
- minutes_until_free = (alloc_info['expires_at'] - current_time).total_seconds() / 60
624
- status += f" (Allocated in memory to {alloc_info['student_id']}, free in {minutes_until_free:.1f} min)"
625
 
626
- # Check if space is recently allocated (local)
627
- is_recently_allocated_local, local_student = local_tracker.is_recently_allocated_locally(space_name)
628
- if is_recently_allocated_local:
629
- status += f" (Recently allocated locally to {local_student})"
630
 
631
- space_activity.append({
632
- 'space_name': space_name,
633
- 'last_activity': last_activity,
634
- 'last_activity_str': last_activity_str,
635
- 'idle_minutes': idle_minutes,
636
- 'status': status,
637
- 'is_recently_allocated_memory': is_recently_allocated_memory,
638
- 'is_recently_allocated_local': is_recently_allocated_local,
639
- 'cached_status': cached_status
640
- })
641
 
642
- print(f"[analyze_space_activity_cached] {space_name}: {status}")
643
-
644
- # Sort by idle time (most idle first)
645
- space_activity.sort(key=lambda x: x['idle_minutes'], reverse=True)
646
-
647
- return space_activity
648
-
649
- def create_status_display(space_activity):
650
- """Create formatted status display for all spaces with proper line breaks"""
651
- status_display = "📊 **Current Space Status (sorted by availability):**<br><br>"
652
-
653
- # 显示内存分配记录摘要
654
- memory_summary = memory_allocation_store.get_status_summary()
655
- if memory_summary['active_count'] > 0:
656
- status_display += f"🧠 **Memory Allocations ({memory_summary['active_count']} active):**<br>"
657
- for alloc in memory_summary['summary']:
658
- status_display += f"&nbsp;&nbsp;&nbsp;• {alloc}<br>"
659
- status_display += "<br>"
660
-
661
- # 显示本地分配记录摘要
662
- local_summary = local_tracker.get_recent_allocations_summary()
663
- if local_summary:
664
- status_display += "🔒 **Recent Local Allocations:**<br>"
665
- for alloc in local_summary:
666
- status_display += f"&nbsp;&nbsp;&nbsp;• {alloc}<br>"
667
- status_display += "<br>"
668
-
669
- for i, space in enumerate(space_activity, 1):
670
- status_display += f"{i}. **{space['space_name']}**<br>"
671
- status_display += f"&nbsp;&nbsp;&nbsp;• Status: {space['status']}<br>"
672
- status_display += f"&nbsp;&nbsp;&nbsp;• Last activity: {space['last_activity_str']}<br><br>"
673
-
674
- return status_display
675
 
676
- def select_space_with_enhanced_collision_avoidance_cached(space_activity, student_id):
677
- """使用缓存的增强防撞空间选择函数 - 完全使用内存存储"""
678
-
679
- print(f"[select_space_with_enhanced_collision_avoidance_cached] Starting selection for student: {student_id}")
680
-
681
- # 第一步:过滤掉不符合基本条件的空间
682
- basic_available_spaces = []
683
- for space in space_activity:
684
- # 检查基本条件 - 使用内存分配记录
685
- if (space['idle_minutes'] >= MIN_IDLE_MINUTES and
686
- not space['is_recently_allocated_memory'] and
687
- not space['is_recently_allocated_local']):
688
- basic_available_spaces.append(space)
689
-
690
- print(f"[select_space_with_enhanced_collision_avoidance_cached] Basic available spaces: {len(basic_available_spaces)}")
691
-
692
- if not basic_available_spaces:
693
- # 生成详细的错误信息
694
- idle_spaces = [s for s in space_activity if s['idle_minutes'] >= MIN_IDLE_MINUTES]
695
- memory_allocated = [s for s in space_activity if s['is_recently_allocated_memory']]
696
- local_allocated = [s for s in space_activity if s['is_recently_allocated_local']]
697
-
698
- error_parts = []
699
- if not idle_spaces:
700
- error_parts.append(f"all spaces used within {MIN_IDLE_MINUTES} minutes")
701
- if memory_allocated:
702
- error_parts.append(f"{len(memory_allocated)} spaces allocated in memory")
703
- if local_allocated:
704
- error_parts.append(f"{len(local_allocated)} spaces locally allocated")
705
 
706
- error_msg = (
707
- f"🚫 **All learning assistants are currently busy**\n\n"
708
- f"Blocking conditions: {', '.join(error_parts)}\n\n"
709
- f"**Please try again in 1-2 minutes.**"
710
- )
711
 
712
- print(f"[select_space_with_enhanced_collision_avoidance_cached] No available spaces: {error_msg}")
713
- raise gr.Error(error_msg, duration=10)
714
-
715
- # 第二步:选择最优空间并进行最终验证
716
- selected_space = basic_available_spaces[0] # 已经按idle_time排序
717
- space_name = selected_space['space_name']
718
-
719
- print(f"[select_space_with_enhanced_collision_avoidance_cached] Preliminary selection: {space_name}")
720
-
721
- # 第三步:最终防撞车检查 - 再次验证本地分配状态
722
- is_local_conflict, conflicting_student = local_tracker.is_recently_allocated_locally(space_name)
723
- if is_local_conflict:
724
- print(f"[select_space_with_enhanced_collision_avoidance_cached] COLLISION DETECTED! {space_name} recently allocated to {conflicting_student}")
725
 
726
- # 寻找替代空间
727
- alternative_spaces = [s for s in basic_available_spaces[1:]
728
- if not local_tracker.is_recently_allocated_locally(s['space_name'])[0]]
 
 
 
 
 
 
 
 
 
 
 
 
729
 
730
- if alternative_spaces:
731
- selected_space = alternative_spaces[0]
732
- space_name = selected_space['space_name']
733
- print(f"[select_space_with_enhanced_collision_avoidance_cached] Using alternative space: {space_name}")
734
- else:
735
- error_msg = (
736
- f"🚫 **Collision detected and no alternatives available**\n\n"
737
- f"The system detected a potential conflict with another student's allocation.\n\n"
738
- f"**Please try again in 10-15 seconds.**"
739
- )
740
- print(f"[select_space_with_enhanced_collision_avoidance_cached] No alternatives available")
741
- raise gr.Error(error_msg, duration=8)
742
-
743
- # 第四步:立即记录本地分配(在写入内存之前)
744
- local_tracker.record_local_allocation(space_name, student_id)
745
- print(f"[select_space_with_enhanced_collision_avoidance_cached] Local allocation recorded BEFORE memory write")
746
-
747
- # 第五步:记录到内存存储 - 不再写入文件或推送到远程
748
- memory_allocation_store.add_allocation(space_name, student_id)
749
- print(f"[select_space_with_enhanced_collision_avoidance_cached] Allocation recorded in memory only")
750
-
751
- # 第六步:生成结果(使用带缓存信息的状态显示)
752
- status_display = create_status_display_with_cache_info(space_activity)
753
- redirect_url = f"https://huggingface.co/spaces/CIV3283/{space_name}/?check={student_id}"
754
-
755
- print(f"[select_space_with_enhanced_collision_avoidance_cached] Final allocation: {space_name} -> {student_id}")
756
-
757
- return redirect_to_space(redirect_url, selected_space, status_display)
758
 
759
- def redirect_to_space(redirect_url, selected_space, status_display):
760
- """Display redirect information with manual click option"""
761
-
762
- if selected_space['idle_minutes'] == float('inf'):
763
- idle_info = "Never used (completely fresh)"
764
- else:
765
- idle_info = f"{selected_space['idle_minutes']:.1f} minutes"
766
-
767
- # Modified HTML structure - Access section first, then analysis
768
- redirect_html = f"""
769
- <div style="max-width: 900px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
770
- <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);">
771
- <h1 style="margin: 0 0 15px 0; font-size: 28px;">🎯 Learning Assistant Assigned</h1>
772
- <h2 style="margin: 0 0 10px 0; font-weight: normal; font-size: 24px;">{selected_space['space_name']}</h2>
773
- <p style="margin: 0; font-size: 18px; opacity: 0.9;">
774
- ✨ This space was idle for: <strong>{idle_info}</strong>
775
- </p>
776
- </div>
777
 
778
- <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);">
779
- <h2 style="margin-top: 0; color: white;">🚀 Access Your Learning Assistant</h2>
780
- <p style="color: rgba(255,255,255,0.9); font-size: 16px; margin-bottom: 25px;">
781
- Click the button below to access your assigned learning assistant.
782
- </p>
783
-
784
- <a href="{redirect_url}"
785
- target="_blank"
786
- style="display: inline-block; background: rgba(255,255,255,0.15); color: white;
787
- padding: 15px 30px; font-size: 18px; font-weight: bold; text-decoration: none;
788
- border-radius: 25px; border: 2px solid rgba(255,255,255,0.3);
789
- transition: all 0.3s ease; margin-bottom: 20px;"
790
- onmouseover="this.style.background='rgba(255,255,255,0.25)'; this.style.transform='translateY(-2px)'"
791
- onmouseout="this.style.background='rgba(255,255,255,0.15)'; this.style.transform='translateY(0px)'">
792
- ➤ Open Learning Assistant
793
- </a>
794
 
795
- <p style="margin: 15px 0 0 0; color: rgba(255,255,255,0.8); font-size: 14px;">
796
- 💡 Left-click the button above or right-click it and select "Open in new tab"
797
- </p>
798
- </div>
799
 
800
- <div style="background: #f8f9fa; padding: 25px; border-radius: 12px; margin-bottom: 25px; border-left: 4px solid #28a745;">
801
- <h3 style="margin-top: 0; color: #333;">📊 Space Selection Analysis</h3>
802
- <div style="background: white; padding: 20px; border-radius: 8px; font-size: 14px; line-height: 1.8; border: 1px solid #e9ecef;">
803
- {status_display}
804
- </div>
805
- <p style="margin-bottom: 0; color: #666; font-size: 14px; margin-top: 15px;">
806
- <strong>Enhanced Selection Algorithm:</strong> Spaces idle for ≥{MIN_IDLE_MINUTES} minutes, not allocated in memory, and not locally allocated are eligible. The system uses pure memory storage for real-time collision avoidance.
807
- </p>
808
- </div>
809
 
810
- <div style="text-align: center; padding: 20px; background: #f1f3f4; border-radius: 8px; margin-top: 20px;">
811
- <p style="margin: 0; color: #5f6368; font-size: 14px;">
812
- 🔄 Need a different assistant? <a href="javascript:location.reload()" style="color: #1976d2; text-decoration: none;">Refresh this page</a> to get reassigned.
813
- </p>
814
- </div>
815
- </div>
816
- """
817
-
818
- return gr.HTML(redirect_html)
819
-
820
- def load_balance_user_cached(student_id):
821
- """使用缓存的负载均衡函数 - 完全使用内存存储"""
822
- print(f"[load_balance_user_cached] Starting cached load balancing for student ID: {student_id}")
823
-
824
- # 检查系统是否已初始化
825
- if not data_manager.initialized:
826
- raise gr.Error("🚫 System not properly initialized. Please contact administrator.", duration=8)
827
-
828
- # 使用缓存版本的分析函数(不触发数据同步)
829
- space_activity = analyze_space_activity_cached()
830
-
831
- # Select space with enhanced collision avoidance
832
- return select_space_with_enhanced_collision_avoidance_cached(space_activity, student_id)
833
 
834
  def get_url_params(request: gr.Request):
835
  """Extract URL parameters from request"""
@@ -837,344 +395,246 @@ def get_url_params(request: gr.Request):
837
  query_params = dict(request.query_params)
838
  check_id = query_params.get('check', None)
839
  if check_id:
840
- return f"Load Distributor", check_id
841
  else:
842
- return "Load Distributor", None
843
- return "Load Distributor", None
844
 
845
- def create_status_display_with_cache_info(space_activity):
846
- """创建包含缓存信息的状态显示"""
847
- global activity_cache
 
848
 
849
- # 获取缓存状态
850
- cache_info = activity_cache.get_cache_info() if activity_cache else None
851
-
852
- status_display = "📊 **Current Space Status (sorted by availability):**<br><br>"
 
 
 
 
 
 
 
 
 
 
853
 
854
- # 显示缓存信息
855
- if cache_info:
856
- if cache_info['is_fresh']:
857
- cache_status = f" Fresh (updated {cache_info['age_minutes']:.1f} min ago)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
  else:
859
- cache_status = f"⚠️ Stale (updated {cache_info['age_minutes']:.1f} min ago)"
860
-
861
- status_display += f"🔄 **Cache Status:** {cache_status}<br>"
862
- status_display += f"📋 **Cached Spaces:** {cache_info['spaces_count']}<br><br>"
863
-
864
- # 显示内存分配记录摘要
865
- memory_summary = memory_allocation_store.get_status_summary()
866
- if memory_summary['active_count'] > 0:
867
- status_display += f"🧠 **Memory Allocations ({memory_summary['active_count']} active):**<br>"
868
- for alloc in memory_summary['summary']:
869
- status_display += f"&nbsp;&nbsp;&nbsp;• {alloc}<br>"
870
- status_display += "<br>"
871
-
872
- # 显示本地分配记录摘要
873
- local_summary = local_tracker.get_recent_allocations_summary()
874
- if local_summary:
875
- status_display += "🔒 **Recent Local Allocations:**<br>"
876
- for alloc in local_summary:
877
- status_display += f"&nbsp;&nbsp;&nbsp;• {alloc}<br>"
878
- status_display += "<br>"
879
-
880
- for i, space in enumerate(space_activity, 1):
881
- status_display += f"{i}. **{space['space_name']}**<br>"
882
- status_display += f"&nbsp;&nbsp;&nbsp;• Status: {space['status']}<br>"
883
- status_display += f"&nbsp;&nbsp;&nbsp;• Last activity: {space['last_activity_str']}<br>"
884
-
885
- # 显示缓存状态(可选)
886
- if 'cached_status' in space:
887
- status_display += f"&nbsp;&nbsp;&nbsp;• Cache: {space['cached_status']}<br>"
888
-
889
- status_display += "<br>"
890
-
891
- return status_display
892
-
893
- def handle_user_access_cached(request: gr.Request):
894
- """使用缓存的用户访问处理"""
895
- title, check_id = get_url_params(request)
896
 
897
- if not check_id:
898
- # No student ID provided
899
- error_html = """
900
- <div style="max-width: 600px; margin: 50px auto; padding: 30px; text-align: center;
901
- background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 12px;">
902
- <h2 style="color: #856404;">⚠️ Invalid Access</h2>
903
- <p style="color: #856404; font-size: 16px; line-height: 1.6;">
904
- This load distributor requires a valid student ID parameter.<br><br>
905
- <strong>Please access this system through the official link provided in Moodle.</strong>
906
- </p>
907
- </div>
908
- """
909
- return title, gr.HTML(error_html)
910
-
911
- # Valid student ID - perform cached load balancing
912
- try:
913
- result = load_balance_user_cached(check_id)
914
- return title, result
915
- except Exception as e:
916
- # Handle any errors during load balancing
917
- error_html = f"""
918
- <div style="max-width: 600px; margin: 50px auto; padding: 30px; text-align: center;
919
- background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 12px;">
920
- <h2 style="color: #721c24;">🚫 Load Balancing Error</h2>
921
- <p style="color: #721c24; font-size: 16px; line-height: 1.6;">
922
- {str(e)}<br><br>
923
- Please try again in a few moments or contact your instructor if the problem persists.
924
- </p>
925
- </div>
926
- """
927
- return title, gr.HTML(error_html)
928
-
929
- # 管理功能
930
- def get_cache_status():
931
- """获取缓存状态(用于调试或管理)"""
932
- global activity_cache
933
- if activity_cache:
934
- return activity_cache.get_cache_info()
935
- return {"error": "Cache not initialized"}
936
-
937
- def force_cache_update():
938
- """强制更新缓存(用于调试或管理)"""
939
- global activity_cache
940
- if activity_cache:
941
- activity_cache.force_update()
942
- return {"status": "Cache updated"}
943
- return {"error": "Cache not initialized"}
944
-
945
- def get_data_manager_status():
946
- """获取数据管理器状态"""
947
- return {
948
- "initialized": data_manager.initialized,
949
- "last_sync": data_manager.last_data_sync.isoformat() if data_manager.last_data_sync else None,
950
- "spaces_count": len(data_manager.available_spaces),
951
- "repo_connected": data_manager.repo is not None
952
- }
953
-
954
- def force_data_sync():
955
- """强制数据同步"""
956
  try:
957
- data_manager._sync_data()
958
- return {"status": "Data sync completed"}
 
 
 
 
 
 
 
 
 
 
959
  except Exception as e:
960
- return {"error": str(e)}
 
 
961
 
962
- def get_memory_allocation_status():
963
- """获取内存分配状态"""
964
- return memory_allocation_store.get_status_summary()
965
 
966
- def clear_memory_allocations():
967
- """清除所有内存分配记录(管理功能)"""
968
- try:
969
- with memory_allocation_store._lock:
970
- cleared_count = len(memory_allocation_store._allocations)
971
- memory_allocation_store._allocations.clear()
972
- return {"status": f"Cleared {cleared_count} memory allocations"}
973
- except Exception as e:
974
- return {"error": str(e)}
975
-
976
- # 添加可选的管理界面(用于调试)
977
- def create_admin_interface():
978
- """创建管理界面(可选)- 增强版本"""
979
- with gr.Blocks(title="CIV3283 Load Distributor - Admin") as admin_interface:
980
- gr.Markdown("# 🔧 CIV3283 Load Distributor - Admin Panel")
981
-
982
- with gr.Tab("Cache Management"):
983
- with gr.Row():
984
- cache_status_btn = gr.Button("📊 Check Cache Status", variant="secondary")
985
- force_cache_update_btn = gr.Button("🔄 Force Cache Update", variant="primary")
986
-
987
- cache_info_display = gr.JSON(label="Cache Information")
988
- cache_status_message = gr.Markdown("")
989
-
990
- with gr.Tab("Data Management"):
991
- with gr.Row():
992
- data_status_btn = gr.Button("📊 Check Data Manager Status", variant="secondary")
993
- force_data_sync_btn = gr.Button("🔄 Force Data Sync", variant="primary")
994
-
995
- data_info_display = gr.JSON(label="Data Manager Information")
996
- data_status_message = gr.Markdown("")
997
-
998
- with gr.Tab("Memory Allocation"):
999
- with gr.Row():
1000
- memory_status_btn = gr.Button("🧠 Check Memory Allocations", variant="secondary")
1001
- clear_memory_btn = gr.Button("🗑️ Clear All Memory Allocations", variant="stop")
1002
-
1003
- memory_info_display = gr.JSON(label="Memory Allocation Information")
1004
- memory_status_message = gr.Markdown("")
1005
-
1006
- def check_cache_status():
1007
- try:
1008
- status = get_cache_status()
1009
- if 'error' in status:
1010
- return status, "❌ Cache not available"
1011
- else:
1012
- age_min = status.get('age_minutes', 0)
1013
- spaces_count = status.get('spaces_count', 0)
1014
- is_fresh = status.get('is_fresh', False)
1015
-
1016
- if is_fresh:
1017
- message = f"✅ Cache is fresh ({age_min:.1f} min old, {spaces_count} spaces)"
1018
- else:
1019
- message = f"⚠️ Cache is stale ({age_min:.1f} min old, {spaces_count} spaces)"
1020
-
1021
- return status, message
1022
- except Exception as e:
1023
- return {"error": str(e)}, f"❌ Error checking cache: {str(e)}"
1024
-
1025
- def force_update():
1026
- try:
1027
- result = force_cache_update()
1028
- if 'error' in result:
1029
- return result, "❌ Failed to update cache"
1030
- else:
1031
- # 获取更新后的状态
1032
- status = get_cache_status()
1033
- return status, "✅ Cache updated successfully"
1034
- except Exception as e:
1035
- return {"error": str(e)}, f"❌ Error updating cache: {str(e)}"
1036
-
1037
- def check_data_status():
1038
- try:
1039
- status = get_data_manager_status()
1040
- if status['initialized']:
1041
- last_sync = status['last_sync']
1042
- if last_sync:
1043
- sync_time = datetime.fromisoformat(last_sync)
1044
- age_minutes = (datetime.now() - sync_time).total_seconds() / 60
1045
- message = f"✅ Data manager active (last sync: {age_minutes:.1f} min ago, {status['spaces_count']} spaces)"
1046
- else:
1047
- message = "⚠️ Data manager initialized but no sync recorded"
1048
- else:
1049
- message = "❌ Data manager not initialized"
1050
- return status, message
1051
- except Exception as e:
1052
- return {"error": str(e)}, f"❌ Error checking data manager: {str(e)}"
1053
-
1054
- def force_sync():
1055
- try:
1056
- result = force_data_sync()
1057
- if 'error' in result:
1058
- return result, "❌ Failed to sync data"
1059
- else:
1060
- # 获取更新后的状态
1061
- status = get_data_manager_status()
1062
- return status, "✅ Data sync completed successfully"
1063
- except Exception as e:
1064
- return {"error": str(e)}, f"❌ Error syncing data: {str(e)}"
1065
-
1066
- def check_memory_status():
1067
- try:
1068
- status = get_memory_allocation_status()
1069
- active_count = status['active_count']
1070
- total_stored = status['total_stored']
1071
 
1072
- if active_count > 0:
1073
- message = f"🧠 {active_count} active allocations ({total_stored} total in memory)"
1074
- else:
1075
- message = " No active memory allocations"
1076
 
1077
- return status, message
1078
- except Exception as e:
1079
- return {"error": str(e)}, f"❌ Error checking memory allocations: {str(e)}"
1080
-
1081
- def clear_memory():
1082
- try:
1083
- result = clear_memory_allocations()
1084
- if 'error' in result:
1085
- return result, " Failed to clear memory allocations"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  else:
1087
- # 获取更新后的状态
1088
- status = get_memory_allocation_status()
1089
- return status, f"✅ {result['status']}"
1090
- except Exception as e:
1091
- return {"error": str(e)}, f"❌ Error clearing memory: {str(e)}"
1092
-
1093
- # Cache tab event handlers
1094
- cache_status_btn.click(
1095
- fn=check_cache_status,
1096
- outputs=[cache_info_display, cache_status_message]
1097
- )
1098
-
1099
- force_cache_update_btn.click(
1100
- fn=force_update,
1101
- outputs=[cache_info_display, cache_status_message]
 
 
1102
  )
1103
 
1104
- # Data tab event handlers
1105
- data_status_btn.click(
1106
- fn=check_data_status,
1107
- outputs=[data_info_display, data_status_message]
1108
  )
1109
 
1110
- force_data_sync_btn.click(
1111
- fn=force_sync,
1112
- outputs=[data_info_display, data_status_message]
1113
- )
1114
-
1115
- # Memory tab event handlers
1116
- memory_status_btn.click(
1117
- fn=check_memory_status,
1118
- outputs=[memory_info_display, memory_status_message]
1119
- )
1120
-
1121
- clear_memory_btn.click(
1122
- fn=clear_memory,
1123
- outputs=[memory_info_display, memory_status_message]
1124
  )
1125
 
1126
- return admin_interface
1127
 
1128
- # 修改后的主Gradio界面
1129
- def create_main_interface():
1130
- """创建主要的用户界面"""
1131
- with gr.Blocks(title="CIV3283 Load Distributor") as interface:
1132
- title_display = gr.Markdown("# 🔄 CIV3283 Learning Assistant Load Distributor", elem_id="title")
1133
- content_display = gr.HTML("")
1134
-
1135
- # 主要的负载均衡逻辑 - 页面加载时执行
1136
- interface.load(
1137
- fn=handle_user_access_cached,
1138
- outputs=[title_display, content_display]
1139
- )
1140
-
1141
- return interface
1142
-
1143
- # 修改后的main函数
1144
  if __name__ == "__main__":
1145
- import sys
1146
-
1147
- try:
1148
- # 系统初始化 - 只在启动时执行一次
1149
- print("🚀 Initializing CIV3283 Load Distributor System...")
1150
- init_system()
1151
- print("✅ System initialization completed successfully")
1152
- print("🧠 Using pure memory storage for allocations (no file I/O during user access)")
1153
-
1154
- # 检查是否需要管理界面
1155
- enable_admin = "--admin" in sys.argv or os.environ.get("ENABLE_ADMIN", "false").lower() == "true"
1156
-
1157
- if enable_admin:
1158
- print("🔧 Starting with admin interface enabled")
1159
- # 创建带管理界面的组合界面
1160
- main_interface = create_main_interface()
1161
- admin_interface = create_admin_interface()
1162
-
1163
- # 使用TabbedInterface组合两个界面
1164
- combined_interface = gr.TabbedInterface(
1165
- [main_interface, admin_interface],
1166
- ["🎯 Load Distributor", "🔧 Admin Panel"],
1167
- title="CIV3283 Load Distributor System"
1168
- )
1169
- combined_interface.launch()
1170
- else:
1171
- print("🎯 Starting main interface only")
1172
- # 只启动主界面
1173
- main_interface = create_main_interface()
1174
- main_interface.launch()
1175
-
1176
- except Exception as e:
1177
- print(f"❌ System initialization failed: {e}")
1178
- import traceback
1179
- print(f"Traceback: {traceback.format_exc()}")
1180
- raise
 
1
  import gradio as gr
 
2
  import csv
3
+ import os
4
+ import re
5
  from datetime import datetime, timedelta
6
  from huggingface_hub import Repository
7
+ from RAG_Learning_Assistant_with_Streaming import RAGLearningAssistant
 
 
8
 
9
+ # Configuration for Student Space
10
+ # find name of space
11
+ def get_space_name():
12
+ space_id = os.environ.get("SPACE_ID", None)
13
+ if space_id:
14
+ # SPACE_ID usually "username/space-name",we only need space-name
15
+ return space_id.split("/")[-1]
16
+ STUDENT_SPACE_NAME = get_space_name() # get space name automatically
17
+ DATA_STORAGE_REPO = "CIV3283/Data_Storage" # Centralized data storage repo
18
  DATA_BRANCH_NAME = "data_branch"
19
+ LOCAL_DATA_DIR = "temp_data_repo"
20
+
21
+ # Session timeout configuration (in minutes)
22
+ SESSION_TIMEOUT_MINUTES = 30 # Adjust this value as needed
23
+
24
+ # File names in data storage
25
+ KNOWLEDGE_FILE = "knowledge_base.md"
26
+ VECTOR_DB_FILE = "vector_database.csv"
27
+ METADATA_FILE = "vector_metadata.json"
28
+ VECTORIZER_FILE = "vectorize_knowledge_base.py"
29
+
30
+ # Student-specific log files (with space name prefix)
31
+ QUERY_LOG_FILE = f"{STUDENT_SPACE_NAME}_query_log.csv"
32
+ FEEDBACK_LOG_FILE = f"{STUDENT_SPACE_NAME}_feedback_log.csv"
33
+
34
+ # Environment variables
35
+ HF_HUB_TOKEN = os.environ.get("HF_HUB_TOKEN", None)
36
+ if HF_HUB_TOKEN is None:
37
+ raise ValueError("Set HF_HUB_TOKEN in Space Settings -> Secrets")
38
+
39
+ OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", None)
40
+ if OPENAI_API_KEY is None:
41
+ raise ValueError("Set OPENAI_API_KEY in Space Settings -> Secrets")
42
 
43
+ MODEL = "gpt-4.1-nano-2025-04-14"
 
 
 
 
44
 
45
+ def check_session_validity(check_id):
46
+ """
47
+ Check if the current session is valid based on:
48
+ 1. If user ID matches last query → Allow continue
49
+ 2. If user ID doesn't match Check time interval:
50
+ - If time interval is small → Block (previous user just finished)
51
+ - If time interval is large → Allow (assistant has been idle)
52
+
53
+ Returns:
54
+ tuple: (is_valid: bool, error_message: str)
55
+ """
56
+ try:
57
+ filepath = os.path.join(LOCAL_DATA_DIR, QUERY_LOG_FILE)
58
 
59
+ # If no log file exists, this is the first query - allow it
60
+ if not os.path.exists(filepath):
61
+ print(f"[check_session_validity] No existing log file, allowing first query for student {check_id}")
62
+ return True, ""
 
 
 
 
 
63
 
64
+ # Read the last record from the CSV file
65
+ with open(filepath, 'r', encoding='utf-8') as csvfile:
66
+ reader = csv.reader(csvfile)
67
+ rows = list(reader)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
+ # If only header exists, this is effectively the first query
70
+ if len(rows) <= 1:
71
+ print(f"[check_session_validity] Only header in log file, allowing first query for student {check_id}")
72
+ return True, ""
73
 
74
+ # Get the last record (most recent query)
75
+ last_record = rows[-1]
 
 
 
 
 
 
 
 
76
 
77
+ # CSV format: [student_space, student_id, timestamp, search_info, query_and_response, thumb_feedback]
78
+ if len(last_record) < 3:
79
+ print(f"[check_session_validity] Invalid last record format, allowing query")
80
+ return True, ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
+ last_student_id = last_record[1]
83
+ last_timestamp_str = last_record[2]
 
 
 
 
 
 
84
 
85
+ print(f"[check_session_validity] Last record - Student ID: {last_student_id}, Timestamp: {last_timestamp_str}")
86
+ print(f"[check_session_validity] Current request - Student ID: {check_id}")
 
87
 
88
+ # If student ID matches, allow continuation
89
+ if last_student_id == check_id:
90
+ print(f"[check_session_validity] Same user, allowing continuation for student {check_id}")
91
+ return True, ""
92
 
93
+ # If student ID doesn't match, check time interval
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  try:
95
+ last_timestamp = datetime.strptime(last_timestamp_str, '%Y-%m-%d %H:%M:%S')
96
+ current_timestamp = datetime.now()
97
+ time_diff = current_timestamp - last_timestamp
98
 
99
+ print(f"[check_session_validity] Different user - Time difference: {time_diff.total_seconds()} seconds ({time_diff.total_seconds()/60:.1f} minutes)")
 
 
100
 
101
+ # If time difference is small, block access (previous user just finished)
102
+ if time_diff <= timedelta(minutes=SESSION_TIMEOUT_MINUTES):
103
+ error_msg = "⚠️ The assistant is currently being used by another user. Please return to the load distributor page."
104
+ print(f"[check_session_validity] Blocking access - Previous user ({last_student_id}) used assistant {time_diff.total_seconds()/60:.1f} minutes ago")
105
+ return False, error_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
+ # If time difference is large, allow access (assistant has been idle)
108
+ print(f"[check_session_validity] Assistant has been idle for {time_diff.total_seconds()/60:.1f} minutes, allowing new user {check_id}")
109
+ return True, ""
110
 
111
+ except ValueError as e:
112
+ print(f"[check_session_validity] Error parsing timestamp: {e}")
113
+ # If we can't parse the timestamp, allow the query to proceed
114
+ return True, ""
 
 
 
 
 
 
 
 
 
 
 
115
 
116
+ except Exception as e:
117
+ print(f"[check_session_validity] Error checking session validity: {e}")
118
+ import traceback
119
+ print(f"[check_session_validity] Traceback: {traceback.format_exc()}")
120
+ # On error, allow the query to proceed to avoid blocking legitimate users
121
+ return True, ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ def init_data_storage_repo():
124
+ """Initialize connection to centralized data storage repository"""
125
+ try:
126
+ repo = Repository(
127
+ local_dir=LOCAL_DATA_DIR,
128
+ clone_from=DATA_STORAGE_REPO,
129
+ revision=DATA_BRANCH_NAME,
130
+ repo_type="space",
131
+ use_auth_token=HF_HUB_TOKEN
132
+ )
133
+ # Configure git user
134
+ repo.git_config_username_and_email("git_user", f"Student_Space_{STUDENT_SPACE_NAME}")
135
+ repo.git_config_username_and_email("git_email", f"{STUDENT_SPACE_NAME}@student.space")
136
+
137
+ # Pull latest changes
138
+ print(f"[init_data_storage_repo] Pulling latest changes from {DATA_STORAGE_REPO}...")
139
+ repo.git_pull(rebase=True)
140
+
141
+ print(f"[init_data_storage_repo] Successfully connected to data storage repo: {DATA_STORAGE_REPO}")
142
+ print(f"[init_data_storage_repo] Local directory: {LOCAL_DATA_DIR}")
143
+ print(f"[init_data_storage_repo] Branch: {DATA_BRANCH_NAME}")
144
+
145
+ # Check if required files exist
146
+ required_files = [KNOWLEDGE_FILE, VECTOR_DB_FILE, METADATA_FILE]
147
+ for file_name in required_files:
148
+ file_path = os.path.join(LOCAL_DATA_DIR, file_name)
149
+ if os.path.exists(file_path):
150
+ print(f"[init_data_storage_repo] Found required file: {file_name}")
151
+ else:
152
+ print(f"[init_data_storage_repo] Warning: Missing required file: {file_name}")
153
 
154
+ return repo
 
 
 
 
 
 
155
 
156
+ except Exception as e:
157
+ print(f"[init_data_storage_repo] Error initializing repository: {e}")
158
+ import traceback
159
+ print(f"[init_data_storage_repo] Traceback: {traceback.format_exc()}")
160
+ return None
161
+
162
+ def commit_student_logs(commit_message: str):
163
+ """Commit student logs to data storage repository with conflict resolution"""
164
+ if repo is None:
165
+ print("[commit_student_logs] Error: Repository not initialized")
166
+ return False
167
 
168
+ max_retries = 3
169
+ retry_count = 0
 
 
 
170
 
171
+ while retry_count < max_retries:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  try:
173
+ # Check if log files exist before adding
174
+ query_log_path = os.path.join(LOCAL_DATA_DIR, QUERY_LOG_FILE)
175
+ feedback_log_path = os.path.join(LOCAL_DATA_DIR, FEEDBACK_LOG_FILE)
 
 
 
176
 
177
+ files_to_add = []
178
+ if os.path.exists(query_log_path):
179
+ files_to_add.append(QUERY_LOG_FILE)
180
+ print(f"[commit_student_logs] Found query log: {query_log_path}")
 
181
 
182
+ if os.path.exists(feedback_log_path):
183
+ files_to_add.append(FEEDBACK_LOG_FILE)
184
+ print(f"[commit_student_logs] Found feedback log: {feedback_log_path}")
 
 
 
 
 
 
 
 
 
185
 
186
+ if not files_to_add:
187
+ print("[commit_student_logs] No log files to commit")
188
+ return False
 
189
 
190
+ # Add files individually
191
+ for file_name in files_to_add:
192
+ print(f"[commit_student_logs] Adding file: {file_name}")
193
+ repo.git_add(pattern=file_name)
194
 
195
+ # Check if there are changes to commit
196
+ try:
197
+ import subprocess
198
+ result = subprocess.run(
199
+ ["git", "status", "--porcelain"],
200
+ cwd=LOCAL_DATA_DIR,
201
+ capture_output=True,
202
+ text=True,
203
+ check=True
204
+ )
 
 
 
 
 
 
 
 
 
205
 
206
+ if not result.stdout.strip():
207
+ print("[commit_student_logs] No changes to commit")
208
+ return True
 
 
209
 
210
+ print(f"[commit_student_logs] Changes detected: {result.stdout.strip()}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
+ except Exception as status_error:
213
+ print(f"[commit_student_logs] Warning: Could not check git status: {status_error}")
 
 
 
 
 
 
 
 
214
 
215
+ # Commit changes locally first
216
+ print(f"[commit_student_logs] Attempt {retry_count + 1}/{max_retries}: Committing locally: {commit_message}")
217
+ repo.git_commit(commit_message)
218
 
219
+ # Now try to pull and push
220
+ print("[commit_student_logs] Pulling latest changes...")
221
+ repo.git_pull(rebase=True)
222
 
223
+ # Push changes
224
+ print("[commit_student_logs] Pushing to remote...")
225
+ repo.git_push()
 
 
 
 
 
 
226
 
227
+ print(f"[commit_student_logs] Success: {commit_message}")
228
+ return True
 
 
 
 
 
 
 
229
 
230
+ except Exception as e:
231
+ error_msg = str(e)
232
+ print(f"[commit_student_logs] Attempt {retry_count + 1} failed: {error_msg}")
233
 
234
+ # Check if it's a push conflict or pull conflict
235
+ if ("rejected" in error_msg and "fetch first" in error_msg) or ("cannot pull with rebase" in error_msg):
236
+ print("[commit_student_logs] Detected Git conflict, will retry...")
237
+ retry_count += 1
238
+
239
+ if retry_count < max_retries:
240
+ # Try to reset and start fresh
241
  try:
242
+ print("[commit_student_logs] Resetting repository state for retry...")
243
+ # Reset to remote state
244
+ repo.git_reset("--hard", "HEAD~1") # Undo the commit
245
+ repo.git_pull(rebase=True) # Get latest changes
246
+
247
+ # Wait a bit before retrying to avoid rapid conflicts
248
+ import time
249
+ wait_time = retry_count * 2 # 2, 4, 6 seconds
250
+ print(f"[commit_student_logs] Waiting {wait_time} seconds before retry...")
251
+ time.sleep(wait_time)
252
+ continue
253
+
254
+ except Exception as reset_error:
255
+ print(f"[commit_student_logs] Reset failed: {reset_error}")
256
+ # If reset fails, try alternative approach
257
+ try:
258
+ # Alternative: stash changes and pull
259
+ repo.git_stash()
260
+ repo.git_pull(rebase=True)
261
+ repo.git_stash("pop")
262
+ continue
263
+ except Exception as stash_error:
264
+ print(f"[commit_student_logs] Stash approach failed: {stash_error}")
265
+ return False
266
  else:
267
+ print("[commit_student_logs] Max retries reached, giving up")
268
+ return False
269
+ else:
270
+ # Other types of errors, don't retry
271
+ print(f"[commit_student_logs] Non-conflict error, not retrying: {error_msg}")
272
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
+ print("[commit_student_logs] Failed after all retry attempts")
275
+ return False
 
 
276
 
277
+ def save_student_query_to_csv(query, search_info, response, check_id, thumb_feedback=None):
278
+ """Save student query record to centralized CSV file"""
279
+ try:
280
+ # Validate check_id
281
+ if not check_id:
282
+ print("[save_student_query_to_csv] Error: No valid check_id provided")
283
+ return False
284
 
285
+ # Ensure the local data directory exists
286
+ os.makedirs(LOCAL_DATA_DIR, exist_ok=True)
 
 
 
 
 
 
 
287
 
288
+ filepath = os.path.join(LOCAL_DATA_DIR, QUERY_LOG_FILE)
289
+ file_exists = os.path.isfile(filepath)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
+ print(f"[save_student_query_to_csv] Saving to: {filepath}")
292
+ print(f"[save_student_query_to_csv] File exists: {file_exists}")
293
+ print(f"[save_student_query_to_csv] Student ID: {check_id}")
 
 
 
 
294
 
295
+ with open(filepath, 'a', newline='', encoding='utf-8') as csvfile:
296
+ writer = csv.writer(csvfile)
297
+ if not file_exists:
298
+ print("[save_student_query_to_csv] Writing header row")
299
+ writer.writerow(['student_space', 'student_id', 'timestamp', 'search_info', 'query_and_response', 'thumb_feedback'])
300
+
301
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
302
+ query_and_response = f"Query: {query}\nResponse: {response}"
303
+ writer.writerow([STUDENT_SPACE_NAME, check_id, timestamp, search_info, query_and_response, thumb_feedback or ""])
304
 
305
+ print(f"[save_student_query_to_csv] Query saved to local file: {filepath}")
 
 
 
 
 
306
 
307
+ # Commit student logs to data storage
308
+ print("[save_student_query_to_csv] Attempting to commit to remote repository...")
309
+ commit_success = commit_student_logs(f"Add query log from student {check_id} at {timestamp}")
 
310
 
311
+ if commit_success:
312
+ print("[save_student_query_to_csv] Successfully committed to remote repository")
313
+ else:
314
+ print("[save_student_query_to_csv] Failed to commit to remote repository")
 
 
 
 
 
 
315
 
316
+ return True
317
+ except Exception as e:
318
+ print(f"[save_student_query_to_csv] Error: {e}")
319
+ import traceback
320
+ print(f"[save_student_query_to_csv] Traceback: {traceback.format_exc()}")
321
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
+ def update_latest_student_query_feedback(feedback_type, check_id):
324
+ """Update thumb feedback for the latest student query in CSV"""
325
+ try:
326
+ # Validate check_id
327
+ if not check_id:
328
+ print("[update_latest_student_query_feedback] Error: No valid check_id provided")
329
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
+ filepath = os.path.join(LOCAL_DATA_DIR, QUERY_LOG_FILE)
332
+ if not os.path.exists(filepath):
333
+ print("[update_latest_student_query_feedback] Error: Query log file not found")
334
+ return False
 
335
 
336
+ # Read existing data
337
+ rows = []
338
+ with open(filepath, 'r', encoding='utf-8') as csvfile:
339
+ reader = csv.reader(csvfile)
340
+ rows = list(reader)
 
 
 
 
 
 
 
 
341
 
342
+ # Update the last row (most recent query)
343
+ if len(rows) > 1: # Ensure there's at least one data row beyond header
344
+ rows[-1][5] = feedback_type # thumb_feedback column (index 5 for student format)
345
+
346
+ # Write back to file
347
+ with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
348
+ writer = csv.writer(csvfile)
349
+ writer.writerows(rows)
350
+
351
+ print(f"[update_latest_student_query_feedback] Updated feedback: {feedback_type}")
352
+
353
+ # Commit the update
354
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
355
+ commit_student_logs(f"Update feedback from student {check_id}: {feedback_type} at {timestamp}")
356
+ return True
357
 
358
+ return False
359
+ except Exception as e:
360
+ print(f"[update_latest_student_query_feedback] Error: {e}")
361
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
362
 
363
+ def save_student_comment_feedback(comment, check_id):
364
+ """Save student comment feedback to centralized feedback file"""
365
+ try:
366
+ # Validate check_id
367
+ if not check_id:
368
+ print("[save_student_comment_feedback] Error: No valid check_id provided")
369
+ return False
 
 
 
 
 
 
 
 
 
 
 
370
 
371
+ filepath = os.path.join(LOCAL_DATA_DIR, FEEDBACK_LOG_FILE)
372
+ file_exists = os.path.isfile(filepath)
373
+
374
+ with open(filepath, 'a', newline='', encoding='utf-8') as csvfile:
375
+ writer = csv.writer(csvfile)
376
+ if not file_exists:
377
+ writer.writerow(['student_space', 'student_id', 'timestamp', 'comment'])
 
 
 
 
 
 
 
 
 
378
 
379
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
380
+ writer.writerow([STUDENT_SPACE_NAME, check_id, timestamp, comment])
 
 
381
 
382
+ print(f"[save_student_comment_feedback] Saved comment to {filepath}")
 
 
 
 
 
 
 
 
383
 
384
+ # Commit student logs
385
+ commit_student_logs(f"Add comment feedback from student {check_id} at {timestamp}")
386
+
387
+ return True
388
+ except Exception as e:
389
+ print(f"[save_student_comment_feedback] Error: {e}")
390
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
 
392
  def get_url_params(request: gr.Request):
393
  """Extract URL parameters from request"""
 
395
  query_params = dict(request.query_params)
396
  check_id = query_params.get('check', None)
397
  if check_id:
398
+ return f"RAG Learning Assistant - Student", check_id
399
  else:
400
+ return "RAG Learning Assistant - Student", None
401
+ return "RAG Learning Assistant - Student", None
402
 
403
+ def chat_response(message, history, search_info_display, check_id, has_query):
404
+ """Process user input and return streaming response"""
405
+ if not message.strip():
406
+ return history, search_info_display, has_query
407
 
408
+ # Check access permission first
409
+ if not check_id:
410
+ print(f"[chat_response] Access denied: No valid check ID provided")
411
+ # Raise error dialog for access denial
412
+ raise gr.Error(
413
+ "⚠️ Access Restricted\n\n"
414
+ "Please access this system through the link provided in Moodle.\n\n"
415
+ "If you are a student in this course:\n"
416
+ "1. Go to your Moodle course page\n"
417
+ "2. Find the 'CivASK' link\n"
418
+ "3. Click the link to access the system\n\n"
419
+ "If you continue to experience issues, please contact your instructor.",
420
+ duration=8
421
+ )
422
 
423
+ # NEW: Check session validity before proceeding
424
+ session_valid, error_message = check_session_validity(check_id)
425
+ if not session_valid:
426
+ print(f"[chat_response] Session invalid for student {check_id}")
427
+ raise gr.Error(error_message, duration=10)
428
+
429
+ # Valid access and valid session - proceed with normal AI conversation
430
+ print(f"[chat_response] Valid access and session for student ID: {check_id}")
431
+
432
+ # Convert to messages format if needed
433
+ if history and isinstance(history[0], list):
434
+ # Convert from tuples to messages format
435
+ messages_history = []
436
+ for user_msg, assistant_msg in history:
437
+ messages_history.append({"role": "user", "content": user_msg})
438
+ if assistant_msg:
439
+ messages_history.append({"role": "assistant", "content": assistant_msg})
440
+ history = messages_history
441
+
442
+ # Add user message
443
+ history.append({"role": "user", "content": message})
444
+ history.append({"role": "assistant", "content": ""})
445
+
446
+ search_info_collected = False
447
+ search_info_content = ""
448
+ content_part = ""
449
+
450
+ # Process streaming response
451
+ for chunk in assistant.generate_response_stream(message):
452
+ if not search_info_collected:
453
+ if "**Response:**" in chunk: # Support English markers
454
+ search_info_content += chunk
455
+ search_info_collected = True
456
+ yield history, search_info_content, has_query
457
+ else:
458
+ search_info_content += chunk
459
+ yield history, search_info_content, has_query
460
  else:
461
+ content_part += chunk
462
+ # Update the last assistant message
463
+ history[-1]["content"] = content_part
464
+ yield history, search_info_content, has_query
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
465
 
466
+ # After streaming is complete, save to CSV (only for valid access)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  try:
468
+ print(f"[chat_response] Saving student query to CSV...")
469
+ print(f"Student Space: {STUDENT_SPACE_NAME}")
470
+ print(f"Student ID: {check_id}")
471
+ print(f"Query: {message}")
472
+
473
+ save_success = save_student_query_to_csv(message, search_info_content, content_part, check_id)
474
+ if save_success:
475
+ print(f"[chat_response] Student query saved successfully")
476
+ has_query = True # Mark that we have a query to rate
477
+ else:
478
+ print(f"[chat_response] Failed to save student query")
479
+
480
  except Exception as e:
481
+ print(f"[chat_response] Error saving student query: {e}")
482
+
483
+ return history, search_info_content, has_query
484
 
485
+ # Global variables
486
+ repo = None
487
+ assistant = None
488
 
489
+ def main():
490
+ """Main function to initialize and launch the student application"""
491
+ global repo, assistant
492
+
493
+ # Initialize data storage repository connection
494
+ repo = init_data_storage_repo()
495
+
496
+ # Initialize RAG assistant with centralized data storage directory
497
+ print(f"[main] Initializing RAG assistant with data directory: {LOCAL_DATA_DIR}")
498
+ print(f"[main] Session timeout set to: {SESSION_TIMEOUT_MINUTES} minutes")
499
+ assistant = RAGLearningAssistant(
500
+ api_key=OPENAI_API_KEY,
501
+ model=MODEL,
502
+ vector_db_path=LOCAL_DATA_DIR # Pass the data storage repo directory
503
+ )
504
+
505
+ print(f"[main] RAG assistant initialized successfully")
506
+ print(f"[main] Student space: {STUDENT_SPACE_NAME}")
507
+ print(f"[main] Data storage repo: {DATA_STORAGE_REPO}")
508
+ print(f"[main] Query log file: {QUERY_LOG_FILE}")
509
+ print(f"[main] Feedback log file: {FEEDBACK_LOG_FILE}")
510
+
511
+ # Create interface
512
+ with gr.Blocks(title=f"RAG Assistant - {STUDENT_SPACE_NAME}") as interface:
513
+ check_id_state = gr.State("1")
514
+ has_query_state = gr.State(False) # Track if there's a query to rate
515
+ title_display = gr.Markdown(f"# RAG Learning Assistant - {STUDENT_SPACE_NAME}", elem_id="title")
516
+
517
+ # Only Query Check functionality for students
518
+ with gr.Row():
519
+ with gr.Column(scale=4):
520
+ chatbot = gr.Chatbot(label="Ask Your Questions", height=500, type="messages", render_markdown=True, latex_delimiters=[
521
+ { "left": "$$", "right": "$$", "display": True },
522
+ { "left": "$", "right": "$", "display": False },
523
+ { "left": "\(", "right": "\)", "display": False },
524
+ { "left": "\[", "right": "\]", "display": True }])
525
+ msg = gr.Textbox(placeholder="Type your message here...", label="Your Message", show_label=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
 
527
+ # Feedback buttons row
528
+ with gr.Row():
529
+ thumbs_up_btn = gr.Button("👍 Good Answer", variant="secondary", size="sm")
530
+ thumbs_down_btn = gr.Button("👎 Poor Answer", variant="secondary", size="sm")
531
 
532
+ feedback_status = gr.Textbox(label="Feedback Status", interactive=False, lines=1)
533
+
534
+ # Comment section
535
+ with gr.Row():
536
+ comment_input = gr.Textbox(placeholder="Share your comments or suggestions...", label="Comments", lines=2)
537
+ submit_comment_btn = gr.Button("Submit Comment", variant="outline")
538
+
539
+ with gr.Column(scale=1):
540
+ search_info = gr.Markdown(label="Search Analysis Information", value="")
541
+
542
+ # Event handlers
543
+ def init_from_url(request: gr.Request):
544
+ title, check_id = get_url_params(request)
545
+ print(f"[init_from_url] Extracted check_id: {check_id}")
546
+ return f"# {title}", check_id, False # Reset has_query state
547
+
548
+ # Feedback handlers
549
+ def handle_thumbs_up(check_id, has_query):
550
+ if not check_id:
551
+ raise gr.Error(
552
+ "⚠️ Access Restricted\n\n"
553
+ "Please access this system through the CivASK link provided in Moodle to use the feedback features.",
554
+ duration=5
555
+ )
556
+
557
+ print(f"[handle_thumbs_up] Student: {STUDENT_SPACE_NAME}, check_id: {check_id}")
558
+
559
+ # Check if student query log exists and has queries
560
+ filepath = os.path.join(LOCAL_DATA_DIR, QUERY_LOG_FILE)
561
+ if os.path.exists(filepath):
562
+ with open(filepath, 'r', encoding='utf-8') as csvfile:
563
+ reader = csv.reader(csvfile)
564
+ rows = list(reader)
565
+ if len(rows) > 1: # Has header + at least one data row
566
+ success = update_latest_student_query_feedback("thumbs_up", check_id)
567
+ return "👍 Thank you for your positive feedback!" if success else "Failed to save feedback"
568
+
569
+ return "No query to rate yet"
570
+
571
+ def handle_thumbs_down(check_id, has_query):
572
+ if not check_id:
573
+ raise gr.Error(
574
+ "⚠️ Access Restricted\n\n"
575
+ "Please access this system through the CivASK link provided in Moodle to use the feedback features.",
576
+ duration=5
577
+ )
578
+
579
+ print(f"[handle_thumbs_down] Student: {STUDENT_SPACE_NAME}, check_id: {check_id}")
580
+
581
+ # Check if student query log exists and has queries
582
+ filepath = os.path.join(LOCAL_DATA_DIR, QUERY_LOG_FILE)
583
+ if os.path.exists(filepath):
584
+ with open(filepath, 'r', encoding='utf-8') as csvfile:
585
+ reader = csv.reader(csvfile)
586
+ rows = list(reader)
587
+ if len(rows) > 1: # Has header + at least one data row
588
+ success = update_latest_student_query_feedback("thumbs_down", check_id)
589
+ return "👎 Thank you for your feedback. We'll work to improve!" if success else "Failed to save feedback"
590
+
591
+ return "No query to rate yet"
592
+
593
+ def handle_comment_submission(comment, check_id):
594
+ if not check_id:
595
+ raise gr.Error(
596
+ "⚠️ Access Restricted\n\n"
597
+ "Please access this system through the CivASK link provided in Moodle to submit comments.",
598
+ duration=5
599
+ )
600
+
601
+ if comment.strip():
602
+ success = save_student_comment_feedback(comment.strip(), check_id)
603
+ if success:
604
+ return "💬 Thank you for your comment!", ""
605
  else:
606
+ return "Failed to save comment", comment
607
+ return "Please enter a comment", comment
608
+
609
+ interface.load(fn=init_from_url, outputs=[title_display, check_id_state, has_query_state])
610
+
611
+ # Query events
612
+ msg.submit(
613
+ chat_response,
614
+ [msg, chatbot, search_info, check_id_state, has_query_state],
615
+ [chatbot, search_info, has_query_state]
616
+ ).then(lambda: "", outputs=[msg])
617
+
618
+ # Feedback events
619
+ thumbs_up_btn.click(
620
+ handle_thumbs_up,
621
+ inputs=[check_id_state, has_query_state],
622
+ outputs=[feedback_status]
623
  )
624
 
625
+ thumbs_down_btn.click(
626
+ handle_thumbs_down,
627
+ inputs=[check_id_state, has_query_state],
628
+ outputs=[feedback_status]
629
  )
630
 
631
+ submit_comment_btn.click(
632
+ handle_comment_submission,
633
+ inputs=[comment_input, check_id_state],
634
+ outputs=[feedback_status, comment_input]
 
 
 
 
 
 
 
 
 
 
635
  )
636
 
637
+ interface.launch()
638
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  if __name__ == "__main__":
640
+ main()