-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathintegrated_workflow.py
More file actions
865 lines (733 loc) · 39.4 KB
/
Copy pathintegrated_workflow.py
File metadata and controls
865 lines (733 loc) · 39.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
"""
統一工作流程管理器 - 整合PDF處理和批改Agent系統
將學號提取、文件歸檔和自動批改串聯成完整流程
"""
import os
import sys
import time
import argparse
from typing import Dict, Any, Optional, List
from datetime import datetime
# 導入PDF處理系統
from complete_pdf_processing_system import PDFProcessingSystem
# 導入獨立考試模式
from exam_pdf_processing_system import ExamPDFProcessingSystem
# 導入批改Agent系統
from agent_registry import registry
from utils import validate_api_key
from csv_module import CSVGradingModule
class IntegratedWorkflowManager:
"""整合工作流程管理器"""
def __init__(self, base_dir: str = "base"):
self.base_dir = base_dir
self.pdf_processor = None
self.workflow_results = {}
def setup_pdf_processor(self, exam_mode: bool = False) -> bool:
"""設置PDF處理器(exam_mode=True 使用獨立考試模式)"""
try:
if exam_mode:
self.pdf_processor = ExamPDFProcessingSystem()
print("✓ 使用獨立考試模式(xlsx 學生名單)")
else:
self.pdf_processor = PDFProcessingSystem()
print("✓ 使用 Moodle 模式(CSV 成績表)")
return True
except Exception as e:
print(f"設置PDF處理器失敗: {e}")
return False
def setup_grading_agents(self) -> bool:
"""設置批改Agent系統"""
try:
# 檢查API金鑰 (改為非強制性的軟性提醒)
if not validate_api_key():
print("⚠️ 提醒: 未檢測到 OPENAI_API_KEY 環境變數。")
print(" (若您使用的是純本地模型或 Gemini 等其他服務,請忽略此訊息;若連線失敗,系統會自動印出錯誤 Log)")
# 【關鍵修改】:拔除原有的 return False,放行讓程式繼續執行
# 自動發現並註冊Agent
agents_dir = "agents"
if os.path.exists(agents_dir):
registry.auto_discover_agents(agents_dir)
# 手動註冊內建Agent(如果agents目錄不存在)
if not registry.list_agents():
try:
from agents.boolean_agent import BooleanAgent
from agents.essay_agent import EssayAgent
from agents.exam_agent import ExamAgent
registry.register_agent(BooleanAgent)
registry.register_agent(EssayAgent)
registry.register_agent(ExamAgent)
except ImportError as e:
print(f"警告: 無法載入部分Agent: {e}")
return len(registry.list_agents()) > 0
except Exception as e:
print(f"設置批改Agent系統失敗: {e}")
return False
def convert_pdf_structure_to_agent_structure(self, pdf_output_folder: str, agent_id: str) -> bool:
"""將PDF處理系統的輸出結構轉換為Agent系統的輸入結構"""
try:
# 創建Agent目錄結構
agent_dir = os.path.join(self.base_dir, agent_id)
student_dir = os.path.join(agent_dir, "student")
teacher_dir = os.path.join(agent_dir, "teacher")
# 確保目錄存在
for directory in [agent_dir, student_dir, teacher_dir]:
os.makedirs(directory, exist_ok=True)
# 檢查PDF處理器的輸出目錄是否存在
if not os.path.exists(pdf_output_folder):
print(f"PDF處理輸出目錄不存在: {pdf_output_folder}")
return False
# 複製學生資料夾到Agent結構中
import shutil
copied_count = 0
for item in os.listdir(pdf_output_folder):
src_path = os.path.join(pdf_output_folder, item)
# 只處理資料夾,且符合學生資料夾格式
if os.path.isdir(src_path) and "_assignsubmission_file" in item:
dest_path = os.path.join(student_dir, item)
# 如果目標已存在,先刪除
if os.path.exists(dest_path):
shutil.rmtree(dest_path)
# 複製整個資料夾
shutil.copytree(src_path, dest_path)
copied_count += 1
print(f"已複製學生資料夾: {item}")
print(f"✓ 成功轉換 {copied_count} 個學生資料夾到Agent結構")
return copied_count > 0
except Exception as e:
print(f"轉換資料夾結構時發生錯誤: {e}")
import traceback
traceback.print_exc()
return False
def _manual_intervention_pause(self, pdf_output_folder: str):
"""人工干預暫停點 - 讓用戶檢查和修正PDF處理結果"""
print("\n" + "="*60)
print("🛠️ 人工干預檢查點")
print("="*60)
print(f"PDF處理已完成,請檢查輸出目錄: {pdf_output_folder}")
print("\n📋 建議檢查事項:")
print(" 1. 檢查失敗案例 - 手動移動檔案到正確的學生資料夾")
print(" 2. 確認學號提取錯誤的檔案是否需要重新命名")
print(" 3. 檢查非完整匹配的案例是否正確分配")
print(" 4. 確認所有學生都有對應的作業檔案")
print(f"\n📁 請打開資料夾進行檢查: {pdf_output_folder}")
# 顯示處理結果摘要
if hasattr(self.pdf_processor, 'extraction_results') and self.pdf_processor.extraction_results:
self._show_processing_summary()
print("\n" + "⚠️"*20)
print("請完成必要的手動調整後,按 Enter 鍵繼續資料夾複製步驟...")
print("如果需要中止流程,請按 Ctrl+C")
print("⚠️"*20)
# ── Phase 3:寬鬆重匹配(選用) ─────────────────────────────────────
print("\n" + "-" * 60)
print("🔓 Phase 3 寬鬆重匹配(選用)")
print(" 將 _unmatched/ 中的剩餘檔案與空學生資料夾再做一次寬鬆匹配")
print(" 適合學號寫得很亂、前兩階段都無法配對的樣本")
print("-" * 60)
try:
p3_choice = input("是否進入 Phase 3?(y = 進入,Enter = 跳過):").strip().lower()
except KeyboardInterrupt:
print("\n\n❌ 使用者中止流程")
raise
if p3_choice == 'y':
if self.pdf_processor and hasattr(self.pdf_processor, 'resolve_unmatched_phase3'):
self.pdf_processor.resolve_unmatched_phase3()
else:
print("⚠️ PDF 處理器不支援 Phase 3,請確認版本。")
else:
print("⏭️ 跳過 Phase 3。")
# ────────────────────────────────────────────────────────────────────
try:
input("\n🔄 按 Enter 繼續,或 Ctrl+C 中止: ")
print("\n✅ 繼續執行資料夾複製步驟...")
except KeyboardInterrupt:
print("\n\n❌ 使用者中止流程")
raise
def _show_processing_summary(self):
"""顯示PDF處理結果摘要"""
results = self.pdf_processor.extraction_results
if not results:
return
success_count = len([r for r in results if r.get('status') == 'success'])
# 🌟 [修改點 1]:將 Phase 2 被踢出的 'unmatched' 狀態也加入失敗/需處理清單
failed_statuses = ['failed', 'student_not_found', 'folder_not_found', 'error', 'unmatched']
failed_count = len([r for r in results if r.get('status') in failed_statuses])
print(f"\n📊 處理結果摘要:")
print(f" ✅ 成功處理: {success_count} 個檔案")
print(f" ❌ 需要處理: {failed_count} 個檔案")
# 🌟 [修改點 2]:新增專屬區塊,攔截 Phase 2 產生的「平手衝突 (tied_conflict)」
tied_conflicts = [r for r in results if r.get('match_type') == 'tied_conflict']
if tied_conflicts:
print(f"\n🤝 Phase 2 重複衝突保留案例 (需手動剔除錯認檔案, 共 {len(tied_conflicts)} 個):")
for result in tied_conflicts:
filename = result.get('original_filename', '未知檔案')
student_name = result.get('student_name', '未知學生')
ai_id = result.get('student_id', '?')
csv_id = result.get('csv_id', '?')
print(f" ⚠️ {filename} → {student_name} AI={ai_id} CSV={csv_id} (平手衝突)")
# 顯示失敗或被移出的檔案
failed_files = [r for r in results if r.get('status') in failed_statuses]
if failed_files:
print(f"\n⚠️ 需要人工處理的檔案 (共 {len(failed_files)} 個):")
for result in failed_files:
filename = result.get('original_filename', '未知檔案')
status = result.get('status', '未知狀態')
student_id = result.get('student_id', '未提取')
status_desc = {
'failed': '學號提取失敗',
'student_not_found': f'找不到學生 (學號: {student_id})',
'folder_not_found': f'資料夾不存在 (學號: {student_id})',
'error': '處理錯誤',
'unmatched': '未能分配或因低分被移出' # 新增 unmatched 的中文說明
}.get(status, status)
print(f" 📄 {filename} - {status_desc}")
# 顯示非完整匹配的檔案 (排除掉 tied_conflict,因為上面已經印過了)
fuzzy_matches = [r for r in results if r.get('status') == 'success' and
r.get('match_type', '').startswith(('fuzzy_', 'complement', 'multiple_candidates_'))]
if fuzzy_matches:
print(f"\n🔍 需要確認的匹配案例 (共 {len(fuzzy_matches)} 個):")
for result in fuzzy_matches:
filename = result.get('original_filename', '未知檔案')
student_name = result.get('student_name', '未知學生')
match_type = result.get('match_type', 'unknown')
ai_id = result.get('student_id', '?')
csv_id = result.get('csv_id', '?')
if match_type.startswith('fuzzy_'):
score = match_type.split('_')[1]
desc = f"模糊匹配 ({score}/7)"
elif match_type == 'complement':
desc = "補全匹配"
elif match_type.startswith('multiple_candidates_'):
count = match_type.split('_')[2]
desc = f"多重候選 ({count}個)"
else:
desc = match_type
print(f" 🔄 {filename} → {student_name} AI={ai_id} CSV={csv_id} ({desc})")
def _update_xlsx_grades_exam(self, agent_id: str, assignment_id: str) -> bool:
"""
Exam 模式:建立新的成績 xlsx,與原始名單學號順序相同,
只包含「學號」「姓名」與本次批改分數欄。
名單中沒有批改結果的學生,分數欄留空。
不修改原始 xlsx。
"""
import re, glob
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from datetime import datetime
# ── 找原始 xlsx ───────────────────────────────────────────────
xlsx_files = glob.glob("*.xlsx")
if not xlsx_files:
print("找不到 xlsx 檔案"); return False
src_path = max(xlsx_files, key=os.path.getmtime)
print(f"讀取來源成績表: {src_path}")
try:
wb_src = openpyxl.load_workbook(src_path, data_only=True)
ws_src = wb_src["Summary"]
except Exception as e:
print(f"載入 xlsx 失敗: {e}"); return False
# 找標頭列的學號/姓名欄位(第 1 列)
header = [cell.value for cell in ws_src[1]]
try:
id_col_idx = header.index("學號") # 0-based
name_col_idx = header.index("姓名")
except ValueError as e:
print(f"xlsx 標頭缺少必要欄位: {e}"); return False
# 讀出所有學生(從第 3 列起,第 2 列為權重)
students = [] # [(student_id, name, src_row)]
for row in ws_src.iter_rows(min_row=3, values_only=True):
sid = row[id_col_idx]
name = row[name_col_idx]
if sid is not None:
students.append((str(sid).strip(), str(name).strip() if name else ""))
if not students:
print("名單為空"); return False
print(f"名單共 {len(students)} 位學生")
# ── 從批改資料夾收集成績 → {學號: 分數} ──────────────────────
student_base = os.path.join(self.base_dir, agent_id, "student")
if not os.path.exists(student_base):
print(f"學生目錄不存在: {student_base}"); return False
scores: dict = {}
for folder_name in os.listdir(student_base):
folder_path = os.path.join(student_base, folder_name)
if not os.path.isdir(folder_path): continue
if "_assignsubmission_file" not in folder_name: continue
sid = folder_name.split("_")[0]
txt_files = glob.glob(os.path.join(folder_path, f"{assignment_id}_exam_grade.txt"))
if not txt_files:
continue
txt_path = max(txt_files, key=os.path.getmtime)
try:
txt = open(txt_path, encoding="utf-8").read()
m = re.search(r"總分:\s*([0-9.]+)", txt)
if m:
scores[sid] = float(m.group(1))
print(f" ✓ 學號 {sid}: {scores[sid]} 分")
else:
print(f" ⚠ {folder_name}: 無法解析總分")
except Exception as e:
print(f" ✗ {folder_name}: 讀取失敗 {e}")
# ── 建立新 xlsx ───────────────────────────────────────────────
wb_new = openpyxl.Workbook()
ws_new = wb_new.active
ws_new.title = assignment_id
score_col_name = f"{assignment_id}_score"
# 標頭樣式
hdr_font = Font(bold=True)
hdr_fill = PatternFill("solid", fgColor="D9E1F2")
hdr_align = Alignment(horizontal="center")
headers = ["學號", "姓名", score_col_name]
for col_idx, h in enumerate(headers, start=1):
cell = ws_new.cell(row=1, column=col_idx, value=h)
cell.font = hdr_font
cell.fill = hdr_fill
cell.alignment = hdr_align
# 調整欄寬
ws_new.column_dimensions["A"].width = 14
ws_new.column_dimensions["B"].width = 12
ws_new.column_dimensions["C"].width = 16
# 填入學生資料(依原始名單順序,分數沒有則留空)
filled, empty = 0, 0
for row_idx, (sid, name) in enumerate(students, start=2):
ws_new.cell(row=row_idx, column=1, value=sid)
ws_new.cell(row=row_idx, column=2, value=name)
if sid in scores:
ws_new.cell(row=row_idx, column=3, value=scores[sid])
filled += 1
else:
# 留空,不填任何值
empty += 1
# 輸出路徑:與來源 xlsx 同目錄,加上 assignment_id 與時間戳
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
src_stem = os.path.splitext(os.path.basename(src_path))[0]
out_name = f"{src_stem}_{assignment_id}_{timestamp}.xlsx"
out_path = os.path.join(os.path.dirname(os.path.abspath(src_path)), out_name)
try:
wb_new.save(out_path)
print(f"\n✓ 已輸出成績表: {out_path}")
print(f" 有成績: {filled} 筆 / 未批改(留空): {empty} 筆")
return filled > 0
except Exception as e:
print(f"儲存新 xlsx 失敗: {e}"); return False
def _update_csv_grades(self, agent_id: str, assignment_id: str) -> bool:
"""更新原始CSV成績表"""
try:
# 創建CSV更新模組實例
csv_module = CSVGradingModule()
# 自動找到CSV檔案
if not csv_module.find_csv_file():
print("找不到原始CSV檔案")
return False
# 載入成績表
if not csv_module.load_gradebook():
print("無法載入CSV檔案")
return False
# 設置Agent資料夾路徑
csv_module.selected_folder = os.path.join(self.base_dir, agent_id)
csv_module.base_path = os.path.join(self.base_dir, agent_id, "student")
print(f"正在從 {csv_module.base_path} 收集成績...")
# 處理成績更新
if not csv_module.process_grades():
print("沒有找到可更新的成績")
return False
# 自動儲存(不詢問用戶)
if csv_module.save_gradebook():
csv_path = csv_module.csv_file_path
updated_count = getattr(csv_module, 'updated_count', 0)
print(f"✓ 成績已更新到原始CSV檔案: {csv_path}")
print(f"✓ 共更新 {updated_count} 位學生的成績")
return True
else:
print("儲存CSV檔案失敗")
return False
except Exception as e:
print(f"更新CSV成績時發生錯誤: {e}")
import traceback
traceback.print_exc()
return False
def _find_latest_pdf_output(self, exam_mode: bool = False) -> Optional[str]:
"""
尋找最新的 PDF 處理輸出目錄。
Moodle 模式:找以 Grades- 開頭的資料夾。
Exam 模式:找含有 _assignsubmission_file 子資料夾的目錄
(exam 系統以 xlsx 檔名命名輸出資料夾)。
"""
try:
SKIP = {"base", "test_results", "workflow_reports",
"transfer_logs", "Extract_PDF", "agents", "model"}
folders = []
for item in os.listdir(os.getcwd()):
if not os.path.isdir(item) or item in SKIP or item.startswith("."):
continue
if exam_mode:
try:
has_student = any("_assignsubmission_file" in sub
for sub in os.listdir(item))
except PermissionError:
has_student = False
if has_student:
folders.append(item)
else:
if (item.startswith("Grades-") or
item.endswith("_submission") or
"_assignsubmission_" in item):
folders.append(item)
if not folders:
return None
folders.sort(key=lambda x: os.path.getmtime(x), reverse=True)
return folders[0]
except Exception as e:
print(f"尋找PDF輸出目錄時發生錯誤: {e}")
return None
def run_complete_workflow(self, agent_id: str, assignment_id: str,
skip_pdf_processing: bool = False,
skip_conversion: bool = False,
skip_manual_check: bool = False,
skip_csv_update: bool = False,
exam_mode: bool = False,
grading_kwargs: Dict[str, Any] = None) -> Dict[str, Any]:
"""執行完整的工作流程"""
print("=== 整合工作流程開始 ===\n")
start_time = time.time()
workflow_results = {
'pdf_processing': {'success': False, 'details': {}},
'structure_conversion': {'success': False, 'details': {}},
'grading': {'success': False, 'details': {}},
'csv_update': {'success': False, 'details': {}}
}
try:
# 步驟1: PDF處理和學號提取(如果需要)
if not skip_pdf_processing:
print("步驟1: 執行PDF處理和學號提取")
print("-" * 40)
if not self.pdf_processor:
if not self.setup_pdf_processor(exam_mode=exam_mode):
workflow_results['pdf_processing']['details']['error'] = "無法設置PDF處理器"
return workflow_results
# 執行PDF處理流程
pdf_success = self.pdf_processor.run()
workflow_results['pdf_processing']['success'] = pdf_success
if pdf_success:
pdf_output_folder = self.pdf_processor.output_folder
workflow_results['pdf_processing']['details'] = {
'output_folder': pdf_output_folder,
'extracted_files': len(self.pdf_processor.extraction_results) if self.pdf_processor.extraction_results else 0
}
print(f"✓ PDF處理完成,輸出目錄: {pdf_output_folder}")
# 人工干預暫停點(可選跳過)
if not skip_manual_check:
self._manual_intervention_pause(pdf_output_folder)
else:
print("⚠️ 跳過人工檢查步驟(--skip-manual-check)")
else:
workflow_results['pdf_processing']['details']['error'] = "PDF處理失敗"
print("✗ PDF處理失敗,工作流程終止")
return workflow_results
else:
print("步驟1: 跳過PDF處理(使用現有結構)")
# 尋找最新的PDF處理輸出目錄
pdf_output_folder = self._find_latest_pdf_output(exam_mode=exam_mode)
if pdf_output_folder:
workflow_results['pdf_processing']['success'] = True
workflow_results['pdf_processing']['details'] = {
'output_folder': pdf_output_folder,
'skipped': True
}
print(f"✓ 使用現有PDF處理結果: {pdf_output_folder}")
else:
workflow_results['pdf_processing']['details']['error'] = "找不到現有PDF處理結果"
print("✗ 找不到現有PDF處理結果")
return workflow_results
# 步驟2: 轉換資料夾結構
if not skip_conversion:
print(f"\n步驟2: 轉換資料夾結構到Agent系統")
print("-" * 40)
conversion_success = self.convert_pdf_structure_to_agent_structure(
pdf_output_folder, agent_id
)
workflow_results['structure_conversion']['success'] = conversion_success
if conversion_success:
workflow_results['structure_conversion']['details'] = {
'agent_dir': os.path.join(self.base_dir, agent_id),
'student_dir': os.path.join(self.base_dir, agent_id, "student")
}
print("✓ 資料夾結構轉換完成")
else:
workflow_results['structure_conversion']['details']['error'] = "資料夾結構轉換失敗"
print("✗ 資料夾結構轉換失敗,工作流程終止")
return workflow_results
else:
print(f"\n步驟2: 跳過資料夾結構轉換(--skip-conversion)")
workflow_results['structure_conversion']['success'] = True
workflow_results['structure_conversion']['details']['skipped'] = True
# 步驟3: 執行自動批改
print(f"\n步驟3: 執行 {agent_id.upper()} Agent 自動批改")
print("-" * 40)
# 設置批改Agent系統
if not self.setup_grading_agents():
workflow_results['grading']['details']['error'] = "無法設置批改Agent系統"
print("✗ 無法設置批改Agent系統")
return workflow_results
# 獲取指定的Agent
agent = registry.get_agent(agent_id, self.base_dir)
if not agent:
workflow_results['grading']['details']['error'] = f"找不到Agent '{agent_id}'"
print(f"✗ 找不到Agent '{agent_id}'")
return workflow_results
# 執行批改
if grading_kwargs is None:
grading_kwargs = {}
grading_results = agent.run_grading(assignment_id, **grading_kwargs)
if grading_results:
workflow_results['grading']['success'] = True
workflow_results['grading']['details'] = {
'processed_count': len(grading_results),
'results': grading_results
}
print(f"✓ 批改完成,處理了 {len(grading_results)} 份作業")
else:
workflow_results['grading']['details'] = {
'processed_count': 0,
'message': '沒有需要批改的新作業'
}
print("⚠ 沒有需要批改的新作業")
# 步驟4: 更新成績表
if not skip_csv_update:
print(f"\n步驟4: 更新成績表")
print("-" * 40)
if exam_mode:
csv_update_success = self._update_xlsx_grades_exam(agent_id, assignment_id)
else:
csv_update_success = self._update_csv_grades(agent_id, assignment_id)
workflow_results['csv_update']['success'] = csv_update_success
if csv_update_success:
print("✓ CSV成績表更新完成")
else:
print("⚠ CSV成績表更新失敗或跳過")
else:
print(f"\n步驟4: 跳過CSV成績表更新(--skip-csv-update)")
workflow_results['csv_update']['success'] = True
workflow_results['csv_update']['details']['skipped'] = True
# 步驟5: 生成統一報告
print(f"\n步驟5: 生成統一報告")
print("-" * 40)
self._generate_workflow_report(workflow_results, agent_id, assignment_id, start_time)
return workflow_results
except Exception as e:
print(f"\n工作流程執行時發生錯誤: {e}")
import traceback
traceback.print_exc()
workflow_results['error'] = str(e)
return workflow_results
finally:
end_time = time.time()
elapsed = end_time - start_time
print(f"\n=== 整合工作流程完成,總耗時: {elapsed:.1f} 秒 ===")
def _generate_workflow_report(self, results: Dict[str, Any], agent_id: str,
assignment_id: str, start_time: float):
"""生成工作流程報告"""
try:
# 創建報告目錄
reports_dir = os.path.join(self.base_dir, "workflow_reports")
os.makedirs(reports_dir, exist_ok=True)
# 生成報告文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
report_filename = f"workflow_{agent_id}_{assignment_id}_{timestamp}.txt"
report_path = os.path.join(reports_dir, report_filename)
# 寫入報告
with open(report_path, 'w', encoding='utf-8') as f:
f.write("整合工作流程執行報告\n")
f.write("=" * 50 + "\n\n")
f.write(f"執行時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Agent類型: {agent_id}\n")
f.write(f"作業ID: {assignment_id}\n")
f.write(f"總執行時間: {time.time() - start_time:.1f} 秒\n\n")
# PDF處理結果
f.write("PDF處理階段:\n")
f.write("-" * 20 + "\n")
pdf_result = results.get('pdf_processing', {})
if pdf_result.get('success'):
f.write("✓ 成功\n")
details = pdf_result.get('details', {})
if details.get('skipped'):
f.write(" (跳過,使用現有結果)\n")
f.write(f"輸出目錄: {details.get('output_folder', 'N/A')}\n")
f.write(f"處理文件數: {details.get('extracted_files', 'N/A')}\n")
else:
f.write("✗ 失敗\n")
f.write(f"錯誤: {pdf_result.get('details', {}).get('error', 'N/A')}\n")
f.write("\n")
# 結構轉換結果
f.write("結構轉換階段:\n")
f.write("-" * 20 + "\n")
conversion_result = results.get('structure_conversion', {})
if conversion_result.get('success'):
f.write("✓ 成功\n")
details = conversion_result.get('details', {})
f.write(f"Agent目錄: {details.get('agent_dir', 'N/A')}\n")
f.write(f"學生目錄: {details.get('student_dir', 'N/A')}\n")
else:
f.write("✗ 失敗\n")
f.write(f"錯誤: {conversion_result.get('details', {}).get('error', 'N/A')}\n")
f.write("\n")
# 批改結果
f.write("自動批改階段:\n")
f.write("-" * 20 + "\n")
grading_result = results.get('grading', {})
if grading_result.get('success'):
f.write("✓ 成功\n")
details = grading_result.get('details', {})
f.write(f"處理作業數: {details.get('processed_count', 0)}\n")
# 如果有具體結果,統計等第分布
grading_data = details.get('results', {})
if grading_data:
grades = {}
total_score = 0
for result in grading_data.values():
grade = result.get('grade', 'Unknown')
grades[grade] = grades.get(grade, 0) + 1
score = result.get('score', result.get('similarity', 0))
total_score += score
avg_score = total_score / len(grading_data) if grading_data else 0
f.write(f"平均分數: {avg_score:.1f}\n")
f.write("等第分布:\n")
for grade, count in sorted(grades.items()):
f.write(f" {grade}: {count} 人\n")
else:
f.write("✗ 失敗\n")
error = grading_result.get('details', {}).get('error')
message = grading_result.get('details', {}).get('message')
if error:
f.write(f"錯誤: {error}\n")
elif message:
f.write(f"訊息: {message}\n")
print(f"✓ 工作流程報告已保存到: {report_path}")
except Exception as e:
print(f"生成工作流程報告時發生錯誤: {e}")
def list_available_agents(base_dir: str = "base"):
"""列出所有可用的 Agent ID"""
# 先確保 Agent 都被載入
agents_dir = "agents"
if os.path.exists(agents_dir):
registry.auto_discover_agents(agents_dir)
# 如果自動發現失敗,嘗試手動載入內建 Agent
if not registry.list_agents():
try:
from agents.boolean_agent import BooleanAgent
from agents.essay_agent import EssayAgent
from agents.exam_agent import ExamAgent
registry.register_agent(BooleanAgent)
registry.register_agent(EssayAgent)
registry.register_agent(ExamAgent)
except ImportError as e:
print(f"警告: 無法載入部分 Agent: {e}")
agents = registry.list_agents()
if not agents:
print("❌ 沒有可用的 Agent")
print(f" 請確認 agents/ 目錄存在,且 base_dir='{base_dir}' 設定正確")
return
print("=" * 60)
print("可用的批改 Agent")
print("=" * 60)
for agent_id in sorted(agents):
agent = registry.get_agent(agent_id, base_dir)
if agent:
try:
info = agent.get_agent_info()
name = info.get('name', '未知')
desc = info.get('description', '無描述')
print(f" • {agent_id:<15} {name}")
print(f" {desc}")
except Exception:
print(f" • {agent_id}")
else:
print(f" • {agent_id} (無法載入詳細資訊)")
print("=" * 60)
print(f"\n共 {len(agents)} 個 Agent 可用")
print("\n使用範例:")
print(" python integrated_workflow.py boolean 1.1")
print(" python integrated_workflow.py exam midterm --exam")
def main():
"""主函數 - 命令行介面"""
parser = argparse.ArgumentParser(description='整合工作流程管理器')
parser.add_argument('agent_id', nargs='?', default=None,
help='要使用的 Agent ID (例如: boolean, essay, exam)。使用 --list-agents 查看所有可用 ID')
parser.add_argument('assignment_id', nargs='?', default=None,
help='作業ID (例如: 1.1, 1.2, midterm)')
parser.add_argument('--list-agents', action='store_true',
help='列出所有可用的 Agent ID 與描述,然後離開')
# 可選參數
parser.add_argument('--base-dir', default='base', help='基礎目錄路徑')
parser.add_argument('--skip-pdf', action='store_true', help='跳過PDF處理,使用現有結構')
parser.add_argument('--skip-conversion', action='store_true', help='跳過資料夾結構轉換')
parser.add_argument('--skip-manual-check', action='store_true', help='跳過人工檢查暫停點')
parser.add_argument('--skip-csv-update', action='store_true', help='跳過CSV成績表更新')
parser.add_argument('--batch-size', type=int, default=5, help='批改批量大小')
parser.add_argument('--test', action='store_true', help='測試模式')
parser.add_argument('--exam', action='store_true', help='獨立考試模式:從 xlsx 成績表讀取學生名單,不需要 Moodle CSV')
parser.add_argument('--force-regenerate', action='store_true', help='強制重新生成標準答案')
parser.add_argument('--thinking-budget-mode', choices=['fixed', 'ratio'], default=None,
help='exam_agent 思考預算方案:fixed=固定 4096;'
'ratio=max_tokens × 0.8(思考:回答 ≈ 4:1,準確度最高)。'
'不指定則由 agent 互動式詢問')
args = parser.parse_args()
# 優先處理 --list-agents
if args.list_agents:
list_available_agents(base_dir=args.base_dir)
sys.exit(0)
# 改為可選參數後,需要手動檢查必填
if not args.agent_id or not args.assignment_id:
parser.print_help()
print("\n❌ 錯誤: 必須提供 agent_id 和 assignment_id")
print(" 或使用 --list-agents 查看可用的 Agent")
sys.exit(1)
# 創建工作流程管理器
workflow_manager = IntegratedWorkflowManager(base_dir=args.base_dir)
# 準備批改參數
grading_kwargs = {
'mode': 'test' if args.test else 'auto',
'batch_size': args.batch_size
}
if args.force_regenerate:
grading_kwargs['force_regenerate'] = True
# 只在使用者明確指定時才傳,否則交給 agent 自己的預設/互動式選單
if args.thinking_budget_mode:
grading_kwargs['thinking_budget_mode'] = args.thinking_budget_mode
# 執行完整工作流程
try:
results = workflow_manager.run_complete_workflow(
agent_id=args.agent_id,
assignment_id=args.assignment_id,
skip_pdf_processing=args.skip_pdf,
skip_conversion=getattr(args, 'skip_conversion', False),
skip_manual_check=getattr(args, 'skip_manual_check', False),
skip_csv_update=getattr(args, 'skip_csv_update', False),
exam_mode=getattr(args, 'exam', False),
grading_kwargs=grading_kwargs
)
# 顯示最終結果摘要
print("\n" + "=" * 50)
print("工作流程執行摘要")
print("=" * 50)
pdf_success = results.get('pdf_processing', {}).get('success', False)
conversion_success = results.get('structure_conversion', {}).get('success', False)
grading_success = results.get('grading', {}).get('success', False)
csv_success = results.get('csv_update', {}).get('success', False)
print(f"PDF處理: {'✓ 成功' if pdf_success else '✗ 失敗'}")
print(f"結構轉換: {'✓ 成功' if conversion_success else '✗ 失敗'}")
print(f"自動批改: {'✓ 成功' if grading_success else '⚠ 無新作業' if grading_success is False and results.get('grading', {}).get('details', {}).get('processed_count') == 0 else '✗ 失敗'}")
if results.get('csv_update', {}).get('details', {}).get('skipped'):
print(f"CSV更新: ⏭️ 已跳過")
else:
print(f"CSV更新: {'✓ 成功' if csv_success else '✗ 失敗'}")
if grading_success:
count = results.get('grading', {}).get('details', {}).get('processed_count', 0)
print(f"處理作業數: {count}")
if csv_success and not results.get('csv_update', {}).get('details', {}).get('skipped'):
print("📊 成績已自動更新到原始CSV檔案")
except KeyboardInterrupt:
print("\n程式被使用者中斷")
sys.exit(1)
except Exception as e:
print(f"\n執行時發生錯誤: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()