generated from Java-2025Fall/final-vibevault-template
add autograde tests
This commit is contained in:
parent
ba051a1b97
commit
83cd133001
213
autograde/aggregate_final_grade.py
Normal file
213
autograde/aggregate_final_grade.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
aggregate_final_grade.py - 聚合最终成绩脚本
|
||||||
|
将编程测试、报告评分等各部分成绩聚合为最终成绩
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"""解析命令行参数"""
|
||||||
|
parser = argparse.ArgumentParser(description='Final Grade Aggregation Script')
|
||||||
|
parser.add_argument('--programming', required=True, help='编程测试评分结果文件')
|
||||||
|
parser.add_argument('--report', required=True, help='REPORT.md评分结果文件')
|
||||||
|
parser.add_argument('--frontend', required=True, help='FRONTEND.md评分结果文件')
|
||||||
|
parser.add_argument('--out', required=True, help='输出最终成绩文件')
|
||||||
|
parser.add_argument('--summary', required=True, help='输出最终成绩摘要文件')
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def load_grade_file(file_path: str) -> Dict[str, Any]:
|
||||||
|
"""加载评分结果文件"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_grades(
|
||||||
|
programming_grade: Dict[str, Any],
|
||||||
|
report_grade: Dict[str, Any],
|
||||||
|
frontend_grade: Dict[str, Any]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""聚合各部分成绩"""
|
||||||
|
# 各部分权重配置
|
||||||
|
weights = {
|
||||||
|
'programming': 0.6, # 编程测试占60%
|
||||||
|
'report': 0.25, # 报告占25%
|
||||||
|
'frontend': 0.15 # 前端报告占15%
|
||||||
|
}
|
||||||
|
|
||||||
|
# 计算各部分成绩
|
||||||
|
programming_score = programming_grade.get('total', 0.0)
|
||||||
|
report_score = report_grade.get('total', 0.0)
|
||||||
|
frontend_score = frontend_grade.get('total', 0.0)
|
||||||
|
|
||||||
|
# 计算加权平均分
|
||||||
|
final_score = (
|
||||||
|
programming_score * weights['programming'] +
|
||||||
|
report_score * weights['report'] +
|
||||||
|
frontend_score * weights['frontend']
|
||||||
|
)
|
||||||
|
|
||||||
|
# 计算各部分的标准化得分(占总分的百分比)
|
||||||
|
programming_percentage = (programming_score / 100.0) * weights['programming'] * 100.0
|
||||||
|
report_percentage = (report_score / 100.0) * weights['report'] * 100.0
|
||||||
|
frontend_percentage = (frontend_score / 100.0) * weights['frontend'] * 100.0
|
||||||
|
|
||||||
|
# 生成聚合结果
|
||||||
|
result = {
|
||||||
|
'final_score': final_score,
|
||||||
|
'breakdown': {
|
||||||
|
'programming': {
|
||||||
|
'score': programming_score,
|
||||||
|
'percentage': programming_percentage,
|
||||||
|
'weight': weights['programming']
|
||||||
|
},
|
||||||
|
'report': {
|
||||||
|
'score': report_score,
|
||||||
|
'percentage': report_percentage,
|
||||||
|
'weight': weights['report']
|
||||||
|
},
|
||||||
|
'frontend': {
|
||||||
|
'score': frontend_score,
|
||||||
|
'percentage': frontend_percentage,
|
||||||
|
'weight': weights['frontend']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'weights': weights,
|
||||||
|
'details': {
|
||||||
|
'programming': programming_grade,
|
||||||
|
'report': report_grade,
|
||||||
|
'frontend': frontend_grade
|
||||||
|
},
|
||||||
|
'status': 'completed',
|
||||||
|
'timestamp': '2025-12-14T18:00:00Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def generate_summary(aggregate_result: Dict[str, Any]) -> str:
|
||||||
|
"""生成最终成绩摘要"""
|
||||||
|
summary = "# 最终成绩报告\n\n"
|
||||||
|
|
||||||
|
# 总体成绩
|
||||||
|
final_score = aggregate_result['final_score']
|
||||||
|
summary += f"## 总体成绩\n"
|
||||||
|
summary += f"**最终得分**: {final_score:.2f}/100\n\n"
|
||||||
|
|
||||||
|
# 成绩分布
|
||||||
|
summary += f"## 成绩分布\n"
|
||||||
|
summary += "| 部分 | 得分 | 权重 | 占总分百分比 |\n"
|
||||||
|
summary += "|------|------|------|------------|\n"
|
||||||
|
|
||||||
|
for part, data in aggregate_result['breakdown'].items():
|
||||||
|
part_name = {
|
||||||
|
'programming': '编程测试',
|
||||||
|
'report': '后端与系统设计报告',
|
||||||
|
'frontend': '前端界面与交互设计报告'
|
||||||
|
}.get(part, part)
|
||||||
|
|
||||||
|
summary += f"| {part_name} | {data['score']:.2f} | {data['weight']*100:.0f}% | {data['percentage']:.2f} |\n"
|
||||||
|
|
||||||
|
summary += "\n"
|
||||||
|
|
||||||
|
# 详细说明
|
||||||
|
summary += f"## 详细说明\n"
|
||||||
|
|
||||||
|
# 编程测试
|
||||||
|
programming = aggregate_result['details']['programming']
|
||||||
|
programming_total = programming.get('total', 0.0)
|
||||||
|
summary += f"### 编程测试 ({aggregate_result['weights']['programming']*100:.0f}%)\n"
|
||||||
|
summary += f"- 原始得分: {programming_total:.2f}/100\n"
|
||||||
|
summary += f"- 测试通过率: {len([g for g in programming.get('groups', []) if g['passed'] > 0])}/{len(programming.get('groups', []))} 组\n"
|
||||||
|
|
||||||
|
for group in programming.get('groups', []):
|
||||||
|
summary += f" - {group['group_name']}: {group['passed']}/{group['total']} 测试通过\n"
|
||||||
|
|
||||||
|
summary += "\n"
|
||||||
|
|
||||||
|
# 报告评分
|
||||||
|
report = aggregate_result['details']['report']
|
||||||
|
report_total = report.get('total', 0.0)
|
||||||
|
summary += f"### 后端与系统设计报告 ({aggregate_result['weights']['report']*100:.0f}%)\n"
|
||||||
|
summary += f"- 原始得分: {report_total:.2f}/100\n"
|
||||||
|
|
||||||
|
if report.get('feedback'):
|
||||||
|
summary += f"- 反馈摘要: {report['feedback'][:100]}...\n"
|
||||||
|
|
||||||
|
summary += "\n"
|
||||||
|
|
||||||
|
# 前端报告评分
|
||||||
|
frontend = aggregate_result['details']['frontend']
|
||||||
|
frontend_total = frontend.get('total', 0.0)
|
||||||
|
summary += f"### 前端界面与交互设计报告 ({aggregate_result['weights']['frontend']*100:.0f}%)\n"
|
||||||
|
summary += f"- 原始得分: {frontend_total:.2f}/100\n"
|
||||||
|
|
||||||
|
if frontend.get('feedback'):
|
||||||
|
summary += f"- 反馈摘要: {frontend['feedback'][:100]}...\n"
|
||||||
|
|
||||||
|
summary += "\n"
|
||||||
|
|
||||||
|
# 评分等级
|
||||||
|
if final_score >= 90:
|
||||||
|
grade_level = "优秀"
|
||||||
|
elif final_score >= 80:
|
||||||
|
grade_level = "良好"
|
||||||
|
elif final_score >= 70:
|
||||||
|
grade_level = "中等"
|
||||||
|
elif final_score >= 60:
|
||||||
|
grade_level = "及格"
|
||||||
|
else:
|
||||||
|
grade_level = "不及格"
|
||||||
|
|
||||||
|
summary += f"## 评分等级\n"
|
||||||
|
summary += f"根据最终得分 {final_score:.2f}/100,该作业的评分等级为:**{grade_level}**\n"
|
||||||
|
|
||||||
|
# 建议
|
||||||
|
summary += f"## 改进建议\n"
|
||||||
|
summary += "1. 编程测试部分可以进一步提高测试覆盖率\n"
|
||||||
|
summary += "2. 报告内容可以更加详细和系统化\n"
|
||||||
|
summary += "3. 前端设计可以考虑更多用户体验细节\n"
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
# 加载各部分评分结果
|
||||||
|
print(f"📁 加载编程测试评分: {args.programming}")
|
||||||
|
programming_grade = load_grade_file(args.programming)
|
||||||
|
|
||||||
|
print(f"📁 加载报告评分: {args.report}")
|
||||||
|
report_grade = load_grade_file(args.report)
|
||||||
|
|
||||||
|
print(f"📁 加载前端报告评分: {args.frontend}")
|
||||||
|
frontend_grade = load_grade_file(args.frontend)
|
||||||
|
|
||||||
|
# 聚合成绩
|
||||||
|
print("📊 聚合最终成绩...")
|
||||||
|
final_result = aggregate_grades(programming_grade, report_grade, frontend_grade)
|
||||||
|
|
||||||
|
# 保存最终成绩
|
||||||
|
print(f"💾 保存最终成绩: {args.out}")
|
||||||
|
with open(args.out, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(final_result, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# 生成摘要
|
||||||
|
print(f"📝 生成最终成绩摘要: {args.summary}")
|
||||||
|
summary = generate_summary(final_result)
|
||||||
|
|
||||||
|
with open(args.summary, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(summary)
|
||||||
|
|
||||||
|
print(f"✅ 成绩聚合完成! 最终得分: {final_result['final_score']:.2f}/100")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
533
autograde/generate_pdf_report.py
Normal file
533
autograde/generate_pdf_report.py
Normal file
@ -0,0 +1,533 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
generate_pdf_report.py - PDF报告生成脚本
|
||||||
|
将评分结果和报告内容整合生成PDF文件
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# 尝试导入reportlab,如果不存在则显示错误
|
||||||
|
try:
|
||||||
|
from reportlab.lib.pagesizes import A4
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
|
||||||
|
from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer,
|
||||||
|
Table, TableStyle, Image, PageBreak)
|
||||||
|
from reportlab.lib import colors
|
||||||
|
from reportlab.lib.units import cm
|
||||||
|
REPORTLAB_AVAILABLE = True
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
REPORTLAB_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"""解析命令行参数"""
|
||||||
|
parser = argparse.ArgumentParser(description='PDF Report Generation Script')
|
||||||
|
parser.add_argument('--score', required=True, help='最终成绩文件')
|
||||||
|
parser.add_argument('--report', required=True, help='REPORT.md文件路径')
|
||||||
|
parser.add_argument('--frontend', required=True, help='FRONTEND.md文件路径')
|
||||||
|
parser.add_argument('--out', required=True, help='输出PDF文件路径')
|
||||||
|
parser.add_argument('--student-id', required=True, help='学生ID')
|
||||||
|
parser.add_argument('--student-name', required=True, help='学生姓名')
|
||||||
|
parser.add_argument('--class-id', required=True, help='班级ID')
|
||||||
|
parser.add_argument('--assignment-id', required=True, help='作业ID')
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def load_file_content(file_path: str) -> str:
|
||||||
|
"""加载文件内容"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def load_json_file(file_path: str) -> dict:
|
||||||
|
"""加载JSON文件内容"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_to_paragraphs(markdown_text: str) -> list:
|
||||||
|
"""将Markdown文本转换为ReportLab可处理的段落列表"""
|
||||||
|
# 简单的Markdown处理,主要处理标题和段落
|
||||||
|
paragraphs = []
|
||||||
|
lines = markdown_text.split('\n')
|
||||||
|
|
||||||
|
current_paragraph = ""
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 处理标题
|
||||||
|
if line.startswith('# '):
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
paragraphs.append(('h1', line[2:]))
|
||||||
|
elif line.startswith('## '):
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
paragraphs.append(('h2', line[3:]))
|
||||||
|
elif line.startswith('### '):
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
paragraphs.append(('h3', line[4:]))
|
||||||
|
elif line.startswith('#### '):
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
paragraphs.append(('h4', line[5:]))
|
||||||
|
# 处理列表
|
||||||
|
elif line.startswith('- '):
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
paragraphs.append(('list_item', line[2:]))
|
||||||
|
elif line.startswith('* '):
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
paragraphs.append(('list_item', line[2:]))
|
||||||
|
elif line.startswith('1. '):
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
paragraphs.append(('list_item', line[3:]))
|
||||||
|
# 处理代码块
|
||||||
|
elif line.startswith('```'):
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
paragraphs.append(('code_block_start', ''))
|
||||||
|
elif paragraphs and paragraphs[-1][0] == 'code_block_start':
|
||||||
|
if line.startswith('```'):
|
||||||
|
paragraphs.append(('code_block_end', ''))
|
||||||
|
else:
|
||||||
|
paragraphs.append(('code_block', line))
|
||||||
|
# 处理引用
|
||||||
|
elif line.startswith('> '):
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
current_paragraph = ""
|
||||||
|
paragraphs.append(('quote', line[2:]))
|
||||||
|
# 处理粗体和斜体
|
||||||
|
else:
|
||||||
|
current_paragraph += ' ' + line if current_paragraph else line
|
||||||
|
|
||||||
|
if current_paragraph:
|
||||||
|
paragraphs.append(('paragraph', current_paragraph))
|
||||||
|
|
||||||
|
return paragraphs
|
||||||
|
|
||||||
|
|
||||||
|
def generate_pdf_report(args, score_data, report_content, frontend_content):
|
||||||
|
"""生成PDF报告"""
|
||||||
|
if not REPORTLAB_AVAILABLE:
|
||||||
|
print("❌ ERROR: reportlab库未安装,无法生成PDF报告")
|
||||||
|
print("请运行: pip install reportlab")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 创建PDF文档
|
||||||
|
doc = SimpleDocTemplate(args.out, pagesize=A4)
|
||||||
|
story = []
|
||||||
|
|
||||||
|
# 获取样式
|
||||||
|
styles = getSampleStyleSheet()
|
||||||
|
|
||||||
|
# 自定义样式
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomHeading1',
|
||||||
|
parent=styles['Heading1'],
|
||||||
|
fontSize=24,
|
||||||
|
alignment=TA_CENTER,
|
||||||
|
textColor=colors.HexColor('#2c3e50'),
|
||||||
|
spaceAfter=30
|
||||||
|
))
|
||||||
|
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomHeading2',
|
||||||
|
parent=styles['Heading2'],
|
||||||
|
fontSize=18,
|
||||||
|
textColor=colors.HexColor('#34495e'),
|
||||||
|
spaceAfter=15,
|
||||||
|
spaceBefore=15
|
||||||
|
))
|
||||||
|
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomHeading3',
|
||||||
|
parent=styles['Heading3'],
|
||||||
|
fontSize=14,
|
||||||
|
textColor=colors.HexColor('#7f8c8d'),
|
||||||
|
spaceAfter=10
|
||||||
|
))
|
||||||
|
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomParagraph',
|
||||||
|
parent=styles['Normal'],
|
||||||
|
fontSize=12,
|
||||||
|
leading=18,
|
||||||
|
alignment=TA_JUSTIFY,
|
||||||
|
textColor=colors.HexColor('#000000')
|
||||||
|
))
|
||||||
|
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomListItem',
|
||||||
|
parent=styles['CustomParagraph'],
|
||||||
|
leftIndent=20,
|
||||||
|
bulletIndent=10
|
||||||
|
))
|
||||||
|
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='CustomCentered',
|
||||||
|
parent=styles['CustomParagraph'],
|
||||||
|
alignment=TA_CENTER
|
||||||
|
))
|
||||||
|
|
||||||
|
styles.add(ParagraphStyle(
|
||||||
|
name='ScoreStyle',
|
||||||
|
parent=styles['CustomCentered'],
|
||||||
|
fontSize=36,
|
||||||
|
textColor=colors.HexColor('#e74c3c'),
|
||||||
|
fontWeight='bold',
|
||||||
|
spaceAfter=20
|
||||||
|
))
|
||||||
|
|
||||||
|
# 封面
|
||||||
|
story.append(Spacer(1, 5*cm))
|
||||||
|
story.append(Paragraph("作业成绩报告", styles['CustomHeading1']))
|
||||||
|
story.append(Spacer(1, 2*cm))
|
||||||
|
|
||||||
|
# 学生信息
|
||||||
|
student_info = [
|
||||||
|
['学生姓名:', args.student_name],
|
||||||
|
['学生ID:', args.student_id],
|
||||||
|
['班级ID:', args.class_id],
|
||||||
|
['作业ID:', args.assignment_id],
|
||||||
|
['提交日期:', datetime.now().strftime('%Y-%m-%d')]
|
||||||
|
]
|
||||||
|
|
||||||
|
table_style = TableStyle([
|
||||||
|
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
||||||
|
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
|
||||||
|
('FONTSIZE', (0, 0), (-1, -1), 12),
|
||||||
|
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
|
||||||
|
('RIGHTPADDING', (0, 0), (0, -1), 20),
|
||||||
|
])
|
||||||
|
|
||||||
|
info_table = Table(student_info, colWidths=[100, 200])
|
||||||
|
info_table.setStyle(table_style)
|
||||||
|
|
||||||
|
story.append(Spacer(1, 1*cm))
|
||||||
|
story.append(info_table)
|
||||||
|
|
||||||
|
# 最终成绩
|
||||||
|
story.append(Spacer(1, 3*cm))
|
||||||
|
story.append(Paragraph("最终成绩", styles['CustomHeading2']))
|
||||||
|
story.append(Paragraph(f"{score_data['final_score']:.2f}/100", styles['ScoreStyle']))
|
||||||
|
|
||||||
|
# 成绩等级
|
||||||
|
if score_data['final_score'] >= 90:
|
||||||
|
grade = "优秀"
|
||||||
|
grade_color = colors.HexColor('#27ae60')
|
||||||
|
elif score_data['final_score'] >= 80:
|
||||||
|
grade = "良好"
|
||||||
|
grade_color = colors.HexColor('#f39c12')
|
||||||
|
elif score_data['final_score'] >= 70:
|
||||||
|
grade = "中等"
|
||||||
|
grade_color = colors.HexColor('#e67e22')
|
||||||
|
elif score_data['final_score'] >= 60:
|
||||||
|
grade = "及格"
|
||||||
|
grade_color = colors.HexColor('#d35400')
|
||||||
|
else:
|
||||||
|
grade = "不及格"
|
||||||
|
grade_color = colors.HexColor('#c0392b')
|
||||||
|
|
||||||
|
story.append(Paragraph(f"等级: {grade}",
|
||||||
|
ParagraphStyle(name='GradeStyle', parent=styles['CustomCentered'], fontSize=20, textColor=grade_color)))
|
||||||
|
|
||||||
|
story.append(PageBreak())
|
||||||
|
|
||||||
|
# 成绩分布
|
||||||
|
story.append(Paragraph("成绩分布", styles['CustomHeading2']))
|
||||||
|
|
||||||
|
distribution_data = [
|
||||||
|
['部分', '得分', '权重', '占总分百分比']
|
||||||
|
]
|
||||||
|
|
||||||
|
for part, data in score_data['breakdown'].items():
|
||||||
|
part_name = {
|
||||||
|
'programming': '编程测试',
|
||||||
|
'report': '后端与系统设计报告',
|
||||||
|
'frontend': '前端界面与交互设计报告'
|
||||||
|
}.get(part, part)
|
||||||
|
|
||||||
|
distribution_data.append([
|
||||||
|
part_name,
|
||||||
|
f"{data['score']:.2f}",
|
||||||
|
f"{data['weight']*100:.0f}%",
|
||||||
|
f"{data['percentage']:.2f}"
|
||||||
|
])
|
||||||
|
|
||||||
|
distribution_table = Table(distribution_data, colWidths=[150, 60, 60, 100])
|
||||||
|
distribution_table.setStyle(TableStyle([
|
||||||
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||||
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||||
|
('FONTSIZE', (0, 0), (-1, -1), 12),
|
||||||
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8f9fa')),
|
||||||
|
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
|
||||||
|
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
|
||||||
|
('TOPPADDING', (0, 0), (-1, -1), 10),
|
||||||
|
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6'))
|
||||||
|
]))
|
||||||
|
|
||||||
|
story.append(Spacer(1, 1*cm))
|
||||||
|
story.append(distribution_table)
|
||||||
|
story.append(Spacer(1, 2*cm))
|
||||||
|
|
||||||
|
# 编程测试详情
|
||||||
|
story.append(Paragraph("编程测试详情", styles['CustomHeading2']))
|
||||||
|
|
||||||
|
programming = score_data['details']['programming']
|
||||||
|
programming_groups = programming.get('groups', [])
|
||||||
|
|
||||||
|
if programming_groups:
|
||||||
|
prog_data = [['测试组', '通过测试', '总测试数', '得分', '通过率']]
|
||||||
|
|
||||||
|
for group in programming_groups:
|
||||||
|
prog_data.append([
|
||||||
|
group['name'],
|
||||||
|
group['passed'],
|
||||||
|
group['total'],
|
||||||
|
f"{group['score']:.2f}",
|
||||||
|
f"{group['passed']/group['total']*100:.1f}%"
|
||||||
|
])
|
||||||
|
|
||||||
|
prog_table = Table(prog_data, colWidths=[100, 70, 70, 60, 70])
|
||||||
|
prog_table.setStyle(TableStyle([
|
||||||
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||||
|
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||||
|
('FONTSIZE', (0, 0), (-1, -1), 12),
|
||||||
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#f8f9fa')),
|
||||||
|
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#2c3e50')),
|
||||||
|
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
||||||
|
('TOPPADDING', (0, 0), (-1, -1), 8),
|
||||||
|
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#dee2e6'))
|
||||||
|
]))
|
||||||
|
|
||||||
|
story.append(Spacer(1, 1*cm))
|
||||||
|
story.append(prog_table)
|
||||||
|
else:
|
||||||
|
story.append(Paragraph("暂无编程测试详情", styles['CustomParagraph']))
|
||||||
|
|
||||||
|
story.append(Spacer(1, 2*cm))
|
||||||
|
story.append(PageBreak())
|
||||||
|
|
||||||
|
# 后端与系统设计报告
|
||||||
|
story.append(Paragraph("后端与系统设计报告", styles['CustomHeading2']))
|
||||||
|
story.append(Spacer(1, 1*cm))
|
||||||
|
|
||||||
|
report_paragraphs = markdown_to_paragraphs(report_content)
|
||||||
|
in_code_block = False
|
||||||
|
|
||||||
|
for para_type, content in report_paragraphs:
|
||||||
|
if para_type == 'h1':
|
||||||
|
story.append(Paragraph(content, styles['CustomHeading1']))
|
||||||
|
elif para_type == 'h2':
|
||||||
|
story.append(Paragraph(content, styles['CustomHeading2']))
|
||||||
|
elif para_type == 'h3':
|
||||||
|
story.append(Paragraph(content, styles['CustomHeading3']))
|
||||||
|
elif para_type == 'paragraph':
|
||||||
|
story.append(Paragraph(content, styles['CustomParagraph']))
|
||||||
|
story.append(Spacer(1, 0.5*cm))
|
||||||
|
elif para_type == 'list_item':
|
||||||
|
story.append(Paragraph(f'• {content}', styles['CustomListItem']))
|
||||||
|
story.append(Spacer(1, 0.2*cm))
|
||||||
|
elif para_type == 'quote':
|
||||||
|
story.append(Paragraph(content,
|
||||||
|
ParagraphStyle(name='QuoteStyle', parent=styles['CustomParagraph'],
|
||||||
|
leftIndent=20, rightIndent=20,
|
||||||
|
borderLeftWidth=3, borderLeftColor=colors.HexColor('#bdc3c7'))))
|
||||||
|
story.append(Spacer(1, 0.5*cm))
|
||||||
|
elif para_type == 'code_block_start':
|
||||||
|
in_code_block = True
|
||||||
|
story.append(Paragraph('```', styles['CustomParagraph']))
|
||||||
|
elif para_type == 'code_block_end':
|
||||||
|
in_code_block = False
|
||||||
|
story.append(Paragraph('```', styles['CustomParagraph']))
|
||||||
|
story.append(Spacer(1, 0.5*cm))
|
||||||
|
elif para_type == 'code_block' and in_code_block:
|
||||||
|
story.append(Paragraph(content,
|
||||||
|
ParagraphStyle(name='CodeStyle', parent=styles['CustomParagraph'],
|
||||||
|
fontName='Courier', fontSize=10,
|
||||||
|
leftIndent=20, rightIndent=20)))
|
||||||
|
|
||||||
|
story.append(PageBreak())
|
||||||
|
|
||||||
|
# 前端界面与交互设计报告
|
||||||
|
story.append(Paragraph("前端界面与交互设计报告", styles['CustomHeading2']))
|
||||||
|
story.append(Spacer(1, 1*cm))
|
||||||
|
|
||||||
|
frontend_paragraphs = markdown_to_paragraphs(frontend_content)
|
||||||
|
in_code_block = False
|
||||||
|
|
||||||
|
for para_type, content in frontend_paragraphs:
|
||||||
|
if para_type == 'h1':
|
||||||
|
story.append(Paragraph(content, styles['CustomHeading1']))
|
||||||
|
elif para_type == 'h2':
|
||||||
|
story.append(Paragraph(content, styles['CustomHeading2']))
|
||||||
|
elif para_type == 'h3':
|
||||||
|
story.append(Paragraph(content, styles['CustomHeading3']))
|
||||||
|
elif para_type == 'paragraph':
|
||||||
|
story.append(Paragraph(content, styles['CustomParagraph']))
|
||||||
|
story.append(Spacer(1, 0.5*cm))
|
||||||
|
elif para_type == 'list_item':
|
||||||
|
story.append(Paragraph(f'• {content}', styles['CustomListItem']))
|
||||||
|
story.append(Spacer(1, 0.2*cm))
|
||||||
|
elif para_type == 'quote':
|
||||||
|
story.append(Paragraph(content,
|
||||||
|
ParagraphStyle(name='QuoteStyle', parent=styles['CustomParagraph'],
|
||||||
|
leftIndent=20, rightIndent=20,
|
||||||
|
borderLeftWidth=3, borderLeftColor=colors.HexColor('#bdc3c7'))))
|
||||||
|
story.append(Spacer(1, 0.5*cm))
|
||||||
|
elif para_type == 'code_block_start':
|
||||||
|
in_code_block = True
|
||||||
|
story.append(Paragraph('```', styles['CustomParagraph']))
|
||||||
|
elif para_type == 'code_block_end':
|
||||||
|
in_code_block = False
|
||||||
|
story.append(Paragraph('```', styles['CustomParagraph']))
|
||||||
|
story.append(Spacer(1, 0.5*cm))
|
||||||
|
elif para_type == 'code_block' and in_code_block:
|
||||||
|
story.append(Paragraph(content,
|
||||||
|
ParagraphStyle(name='CodeStyle', parent=styles['CustomParagraph'],
|
||||||
|
fontName='Courier', fontSize=10,
|
||||||
|
leftIndent=20, rightIndent=20)))
|
||||||
|
|
||||||
|
# 构建并保存PDF
|
||||||
|
try:
|
||||||
|
doc.build(story)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ PDF生成失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def generate_text_report(args, score_data, report_content, frontend_content):
|
||||||
|
"""生成纯文本报告作为备选方案"""
|
||||||
|
text_report = []
|
||||||
|
text_report.append("=" * 80)
|
||||||
|
text_report.append(" 作业成绩报告 ")
|
||||||
|
text_report.append("=" * 80)
|
||||||
|
text_report.append("")
|
||||||
|
|
||||||
|
# 学生信息
|
||||||
|
text_report.append(f"学生姓名: {args.student_name}")
|
||||||
|
text_report.append(f"学生ID: {args.student_id}")
|
||||||
|
text_report.append(f"班级ID: {args.class_id}")
|
||||||
|
text_report.append(f"作业ID: {args.assignment_id}")
|
||||||
|
text_report.append(f"提交日期: {datetime.now().strftime('%Y-%m-%d')}")
|
||||||
|
text_report.append("")
|
||||||
|
|
||||||
|
# 最终成绩
|
||||||
|
text_report.append("-" * 80)
|
||||||
|
text_report.append(f"最终成绩: {score_data['final_score']:.2f}/100")
|
||||||
|
|
||||||
|
# 成绩等级
|
||||||
|
if score_data['final_score'] >= 90:
|
||||||
|
grade = "优秀"
|
||||||
|
elif score_data['final_score'] >= 80:
|
||||||
|
grade = "良好"
|
||||||
|
elif score_data['final_score'] >= 70:
|
||||||
|
grade = "中等"
|
||||||
|
elif score_data['final_score'] >= 60:
|
||||||
|
grade = "及格"
|
||||||
|
else:
|
||||||
|
grade = "不及格"
|
||||||
|
|
||||||
|
text_report.append(f"等级: {grade}")
|
||||||
|
text_report.append("")
|
||||||
|
|
||||||
|
# 成绩分布
|
||||||
|
text_report.append("成绩分布:")
|
||||||
|
text_report.append("| 部分 | 得分 | 权重 | 占总分百分比 |")
|
||||||
|
text_report.append("|------|------|------|------------|")
|
||||||
|
|
||||||
|
for part, data in score_data['breakdown'].items():
|
||||||
|
part_name = {
|
||||||
|
'programming': '编程测试',
|
||||||
|
'report': '后端与系统设计报告',
|
||||||
|
'frontend': '前端界面与交互设计报告'
|
||||||
|
}.get(part, part)
|
||||||
|
|
||||||
|
text_report.append(f"| {part_name} | {data['score']:.2f} | {data['weight']*100:.0f}% | {data['percentage']:.2f} |")
|
||||||
|
|
||||||
|
text_report.append("")
|
||||||
|
text_report.append("=" * 80)
|
||||||
|
text_report.append("")
|
||||||
|
|
||||||
|
# 保存文本报告
|
||||||
|
text_report_path = args.out.replace('.pdf', '.txt')
|
||||||
|
with open(text_report_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write('\n'.join(text_report))
|
||||||
|
|
||||||
|
print(f"✅ 已生成纯文本报告: {text_report_path}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
print("📁 加载评分数据...")
|
||||||
|
score_data = load_json_file(args.score)
|
||||||
|
|
||||||
|
print("📁 加载后端报告内容...")
|
||||||
|
report_content = load_file_content(args.report)
|
||||||
|
|
||||||
|
print("📁 加载前端报告内容...")
|
||||||
|
frontend_content = load_file_content(args.frontend)
|
||||||
|
|
||||||
|
print("📄 生成PDF报告...")
|
||||||
|
pdf_success = generate_pdf_report(args, score_data, report_content, frontend_content)
|
||||||
|
|
||||||
|
# 如果PDF生成失败,生成纯文本报告
|
||||||
|
if not pdf_success:
|
||||||
|
print("🔄 尝试生成纯文本报告...")
|
||||||
|
generate_text_report(args, score_data, report_content, frontend_content)
|
||||||
|
else:
|
||||||
|
print(f"✅ PDF报告生成成功: {args.out}")
|
||||||
|
|
||||||
|
# 生成一个简单的总结文件
|
||||||
|
summary = {
|
||||||
|
"student_info": {
|
||||||
|
"name": args.student_name,
|
||||||
|
"id": args.student_id,
|
||||||
|
"class_id": args.class_id,
|
||||||
|
"assignment_id": args.assignment_id
|
||||||
|
},
|
||||||
|
"final_score": score_data["final_score"],
|
||||||
|
"report_path": args.out,
|
||||||
|
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"status": "completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
summary_path = args.out.replace('.pdf', '_summary.json')
|
||||||
|
with open(summary_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(summary, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
print(f"✅ 总结文件生成成功: {summary_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
181
autograde/grade_grouped.py
Normal file
181
autograde/grade_grouped.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
grade_grouped.py - 编程测试评分脚本
|
||||||
|
根据测试分组配置对JUnit测试结果进行评分
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"""解析命令行参数"""
|
||||||
|
parser = argparse.ArgumentParser(description='Programming Test Grading Script')
|
||||||
|
parser.add_argument('--junit-dir', required=True, help='JUnit test results directory')
|
||||||
|
parser.add_argument('--groups', required=True, help='Test groups configuration file')
|
||||||
|
parser.add_argument('--out', required=True, help='Output grade file (JSON)')
|
||||||
|
parser.add_argument('--summary', required=True, help='Output summary file (Markdown)')
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def load_test_groups(groups_file: str) -> Dict[str, Dict[str, any]]:
|
||||||
|
"""加载测试分组配置"""
|
||||||
|
with open(groups_file, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_junit_results(junit_dir: str) -> Dict[str, Dict[str, any]]:
|
||||||
|
"""解析JUnit测试结果"""
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# 遍历JUnit目录中的所有XML文件
|
||||||
|
for filename in os.listdir(junit_dir):
|
||||||
|
if filename.endswith('.xml'):
|
||||||
|
filepath = os.path.join(junit_dir, filename)
|
||||||
|
tree = ET.parse(filepath)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
# 命名空间处理
|
||||||
|
ns = {'junit': 'http://www.junit.org/XML/ns/junit'} if '{' in root.tag else {}
|
||||||
|
|
||||||
|
# 遍历所有测试类
|
||||||
|
for testcase in root.findall('.//testcase', ns):
|
||||||
|
class_name = testcase.get('classname')
|
||||||
|
test_name = testcase.get('name')
|
||||||
|
full_test_name = f"{class_name}.{test_name}"
|
||||||
|
|
||||||
|
# 检查是否有失败或错误
|
||||||
|
failure = testcase.find('.//failure', ns)
|
||||||
|
error = testcase.find('.//error', ns)
|
||||||
|
|
||||||
|
results[full_test_name] = {
|
||||||
|
'status': 'failed' if failure else 'error' if error else 'passed',
|
||||||
|
'duration': float(testcase.get('time', '0')),
|
||||||
|
'message': failure.get('message') if failure else error.get('message') if error else ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_grades(test_results: Dict[str, Dict[str, any]], test_groups: Dict[str, Dict[str, any]]) -> Tuple[float, List[Dict[str, any]]]:
|
||||||
|
"""计算最终成绩"""
|
||||||
|
total_score = 0.0
|
||||||
|
group_details = []
|
||||||
|
|
||||||
|
for group_name, group_config in test_groups.items():
|
||||||
|
group_weight = group_config.get('weight', 1.0)
|
||||||
|
group_score = 0.0
|
||||||
|
group_total = 0.0
|
||||||
|
group_passed = 0
|
||||||
|
group_total_tests = 0
|
||||||
|
group_test_details = []
|
||||||
|
|
||||||
|
for test_pattern, test_weight in group_config.get('tests', {}).items():
|
||||||
|
# 查找匹配的测试
|
||||||
|
for test_name, test_result in test_results.items():
|
||||||
|
if test_pattern in test_name:
|
||||||
|
group_total += test_weight
|
||||||
|
group_total_tests += 1
|
||||||
|
|
||||||
|
if test_result['status'] == 'passed':
|
||||||
|
group_score += test_weight
|
||||||
|
group_passed += 1
|
||||||
|
|
||||||
|
group_test_details.append({
|
||||||
|
'test_name': test_name,
|
||||||
|
'status': test_result['status'],
|
||||||
|
'weight': test_weight,
|
||||||
|
'score': test_weight if test_result['status'] == 'passed' else 0,
|
||||||
|
'duration': test_result['duration'],
|
||||||
|
'message': test_result['message']
|
||||||
|
})
|
||||||
|
|
||||||
|
# 计算组内成绩
|
||||||
|
if group_total > 0:
|
||||||
|
group_percentage = (group_score / group_total) * 100
|
||||||
|
weighted_group_score = (group_score / group_total) * group_weight
|
||||||
|
total_score += weighted_group_score
|
||||||
|
|
||||||
|
group_details.append({
|
||||||
|
'group_name': group_name,
|
||||||
|
'weight': group_weight,
|
||||||
|
'score': weighted_group_score,
|
||||||
|
'percentage': group_percentage,
|
||||||
|
'passed': group_passed,
|
||||||
|
'total': group_total_tests,
|
||||||
|
'tests': group_test_details
|
||||||
|
})
|
||||||
|
|
||||||
|
return total_score, group_details
|
||||||
|
|
||||||
|
|
||||||
|
def generate_summary(total_score: float, group_details: List[Dict[str, any]]) -> str:
|
||||||
|
"""生成评分摘要Markdown"""
|
||||||
|
summary = "# 编程测试评分报告\n\n"
|
||||||
|
summary += f"## 最终成绩: {total_score:.2f}\n\n"
|
||||||
|
|
||||||
|
for group in group_details:
|
||||||
|
summary += f"### {group['group_name']}\n"
|
||||||
|
summary += f"- 权重: {group['weight']}\n"
|
||||||
|
summary += f"- 得分: {group['score']:.2f} ({group['percentage']:.1f}%)\n"
|
||||||
|
summary += f"- 通过率: {group['passed']}/{group['total']}\n\n"
|
||||||
|
|
||||||
|
summary += "| 测试名称 | 状态 | 权重 | 得分 | 耗时 (秒) | 错误信息 |\n"
|
||||||
|
summary += "|---------|------|------|------|----------|---------|\n"
|
||||||
|
|
||||||
|
for test in group['tests']:
|
||||||
|
status = "✅ 通过" if test['status'] == 'passed' else "❌ 失败" if test['status'] == 'failed' else "⚠️ 错误"
|
||||||
|
message = test['message'][:50] + "..." if len(test['message']) > 50 else test['message']
|
||||||
|
message = message.replace('|', '\\|').replace('\n', ' ') # 转义Markdown表格字符
|
||||||
|
|
||||||
|
summary += f"| {test['test_name']} | {status} | {test['weight']} | {test['score']:.2f} | {test['duration']:.2f} | {message} |\n"
|
||||||
|
|
||||||
|
summary += "\n"
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
# 加载测试分组配置
|
||||||
|
print(f"📁 加载测试分组配置: {args.groups}")
|
||||||
|
test_groups = load_test_groups(args.groups)
|
||||||
|
|
||||||
|
# 解析JUnit测试结果
|
||||||
|
print(f"🔍 解析JUnit测试结果: {args.junit_dir}")
|
||||||
|
test_results = parse_junit_results(args.junit_dir)
|
||||||
|
|
||||||
|
# 计算成绩
|
||||||
|
print("📊 计算最终成绩...")
|
||||||
|
total_score, group_details = calculate_grades(test_results, test_groups)
|
||||||
|
|
||||||
|
# 生成JSON输出
|
||||||
|
print(f"💾 保存评分结果: {args.out}")
|
||||||
|
output = {
|
||||||
|
'total': total_score,
|
||||||
|
'groups': group_details,
|
||||||
|
'timestamp': '2025-12-14T18:00:00Z',
|
||||||
|
'version': '1.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(args.out, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(output, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# 生成摘要
|
||||||
|
print(f"📝 生成评分摘要: {args.summary}")
|
||||||
|
summary = generate_summary(total_score, group_details)
|
||||||
|
|
||||||
|
with open(args.summary, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(summary)
|
||||||
|
|
||||||
|
print(f"✅ 评分完成! 最终成绩: {total_score:.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
210
autograde/llm_grade.py
Normal file
210
autograde/llm_grade.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
llm_grade.py - 使用LLM对报告进行评分
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
"""解析命令行参数"""
|
||||||
|
parser = argparse.ArgumentParser(description='LLM Report Grading Script')
|
||||||
|
parser.add_argument('--question', required=True, help='评分问题描述')
|
||||||
|
parser.add_argument('--answer', required=True, help='待评分的答案文件')
|
||||||
|
parser.add_argument('--rubric', required=True, help='评分标准文件')
|
||||||
|
parser.add_argument('--out', required=True, help='输出评分结果文件')
|
||||||
|
parser.add_argument('--summary', required=True, help='输出评分摘要文件')
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def load_file_content(file_path: str) -> str:
|
||||||
|
"""加载文件内容"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def load_rubric(rubric_path: str) -> Dict[str, Any]:
|
||||||
|
"""加载评分标准"""
|
||||||
|
with open(rubric_path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def call_llm_api(prompt: str, max_retries: int = 3, timeout: int = 30) -> str:
|
||||||
|
"""调用LLM API"""
|
||||||
|
# 获取环境变量中的API配置
|
||||||
|
api_key = os.environ.get('LLM_API_KEY', '')
|
||||||
|
api_url = os.environ.get('LLM_API_URL', 'http://localhost:11434/api/generate')
|
||||||
|
model = os.environ.get('LLM_MODEL', 'llama3')
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
if api_key:
|
||||||
|
headers['Authorization'] = f'Bearer {api_key}'
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'model': model,
|
||||||
|
'prompt': prompt,
|
||||||
|
'stream': False,
|
||||||
|
'temperature': 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
response = requests.post(api_url, json=payload, headers=headers, timeout=timeout)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
result = response.json()
|
||||||
|
return result.get('response', '')
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
print(f"⚠️ LLM API调用失败 (尝试 {attempt + 1}/{max_retries}): {e}")
|
||||||
|
if attempt < max_retries - 1:
|
||||||
|
time.sleep(2 ** attempt) # 指数退避
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def generate_grading_prompt(question: str, answer: str, rubric: Dict[str, Any]) -> str:
|
||||||
|
"""生成评分提示词"""
|
||||||
|
prompt = f"""你是一位专业的课程作业评分专家。请根据以下评分标准,对学生的作业进行客观、公正的评分。
|
||||||
|
|
||||||
|
## 评分问题
|
||||||
|
{question}
|
||||||
|
|
||||||
|
## 学生答案
|
||||||
|
{answer}
|
||||||
|
|
||||||
|
## 评分标准
|
||||||
|
{json.dumps(rubric, ensure_ascii=False, indent=2)}
|
||||||
|
|
||||||
|
## 评分要求
|
||||||
|
1. 严格按照评分标准进行评分,每个评分项给出具体得分
|
||||||
|
2. 详细说明每个评分项的得分理由
|
||||||
|
3. 给出总体评价和建议
|
||||||
|
4. 最终输出必须包含JSON格式的评分结果,格式如下:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"total": 总分,
|
||||||
|
"scores": {
|
||||||
|
"评分项1": 得分,
|
||||||
|
"评分项2": 得分,
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"feedback": "详细的评分反馈和建议"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
请确保输出格式正确,只包含上述JSON格式内容,不要添加任何其他说明。"""
|
||||||
|
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
|
||||||
|
def parse_llm_response(response: str) -> Dict[str, Any]:
|
||||||
|
"""解析LLM响应"""
|
||||||
|
# 提取JSON部分
|
||||||
|
import re
|
||||||
|
json_match = re.search(r'```json\n(.*?)\n```', response, re.DOTALL)
|
||||||
|
|
||||||
|
if json_match:
|
||||||
|
json_str = json_match.group(1)
|
||||||
|
try:
|
||||||
|
return json.loads(json_str)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("⚠️ LLM响应中的JSON格式错误")
|
||||||
|
|
||||||
|
# 尝试直接解析响应
|
||||||
|
try:
|
||||||
|
return json.loads(response)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print("⚠️ LLM响应不是有效的JSON格式")
|
||||||
|
|
||||||
|
# 如果都失败,返回默认值
|
||||||
|
return {
|
||||||
|
'total': 0.0,
|
||||||
|
'scores': {},
|
||||||
|
'feedback': '评分失败:无法解析LLM响应'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_summary(grade_result: Dict[str, Any], rubric: Dict[str, Any]) -> str:
|
||||||
|
"""生成评分摘要"""
|
||||||
|
summary = "# LLM评分报告\n\n"
|
||||||
|
|
||||||
|
summary += f"## 总体评价\n"
|
||||||
|
summary += f"- 最终得分: {grade_result['total']:.2f}/{sum(rubric.get('criteria', {}).values())}\n"
|
||||||
|
summary += f"- 评分时间: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}\n\n"
|
||||||
|
|
||||||
|
summary += f"## 评分详情\n"
|
||||||
|
summary += "| 评分项 | 得分 | 满分 | 评分标准 |\n"
|
||||||
|
summary += "|-------|------|------|---------|\n"
|
||||||
|
|
||||||
|
for criterion, full_score in rubric.get('criteria', {}).items():
|
||||||
|
score = grade_result['scores'].get(criterion, 0.0)
|
||||||
|
summary += f"| {criterion} | {score:.2f} | {full_score} | {rubric.get('descriptions', {}).get(criterion, '')} |\n"
|
||||||
|
|
||||||
|
summary += "\n"
|
||||||
|
summary += f"## 详细反馈\n"
|
||||||
|
summary += grade_result['feedback'] + "\n"
|
||||||
|
|
||||||
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主函数"""
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
# 加载文件内容
|
||||||
|
print(f"📁 加载待评分文件: {args.answer}")
|
||||||
|
answer_content = load_file_content(args.answer)
|
||||||
|
|
||||||
|
# 加载评分标准
|
||||||
|
print(f"📋 加载评分标准: {args.rubric}")
|
||||||
|
rubric = load_rubric(args.rubric)
|
||||||
|
|
||||||
|
# 生成评分提示词
|
||||||
|
print("📝 生成评分提示词...")
|
||||||
|
prompt = generate_grading_prompt(args.question, answer_content, rubric)
|
||||||
|
|
||||||
|
# 调用LLM API
|
||||||
|
print("🤖 调用LLM进行评分...")
|
||||||
|
try:
|
||||||
|
llm_response = call_llm_api(prompt)
|
||||||
|
print("✅ LLM API调用成功")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ LLM API调用失败: {e}")
|
||||||
|
# 返回默认评分结果
|
||||||
|
grade_result = {
|
||||||
|
'total': 0.0,
|
||||||
|
'scores': {criterion: 0.0 for criterion in rubric.get('criteria', {})},
|
||||||
|
'feedback': f'评分失败:LLM API调用错误 - {str(e)}'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# 解析LLM响应
|
||||||
|
print("📊 解析LLM评分结果...")
|
||||||
|
grade_result = parse_llm_response(llm_response)
|
||||||
|
|
||||||
|
# 保存评分结果
|
||||||
|
print(f"💾 保存评分结果: {args.out}")
|
||||||
|
with open(args.out, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(grade_result, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# 生成评分摘要
|
||||||
|
print(f"📝 生成评分摘要: {args.summary}")
|
||||||
|
summary = generate_summary(grade_result, rubric)
|
||||||
|
|
||||||
|
with open(args.summary, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(summary)
|
||||||
|
|
||||||
|
print(f"✅ 评分完成! 最终得分: {grade_result['total']:.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue
Block a user