From 83cd13300118b0b187f45960d244e78e4f32f057 Mon Sep 17 00:00:00 2001 From: liyitian <2717355959@qq.com> Date: Sun, 14 Dec 2025 18:19:41 +0800 Subject: [PATCH] add autograde tests --- autograde/aggregate_final_grade.py | 213 ++++++++++++ autograde/generate_pdf_report.py | 533 +++++++++++++++++++++++++++++ autograde/grade_grouped.py | 181 ++++++++++ autograde/llm_grade.py | 210 ++++++++++++ 4 files changed, 1137 insertions(+) create mode 100644 autograde/aggregate_final_grade.py create mode 100644 autograde/generate_pdf_report.py create mode 100644 autograde/grade_grouped.py create mode 100644 autograde/llm_grade.py diff --git a/autograde/aggregate_final_grade.py b/autograde/aggregate_final_grade.py new file mode 100644 index 0000000..f0866d9 --- /dev/null +++ b/autograde/aggregate_final_grade.py @@ -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() diff --git a/autograde/generate_pdf_report.py b/autograde/generate_pdf_report.py new file mode 100644 index 0000000..60e4443 --- /dev/null +++ b/autograde/generate_pdf_report.py @@ -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() diff --git a/autograde/grade_grouped.py b/autograde/grade_grouped.py new file mode 100644 index 0000000..c01a0f4 --- /dev/null +++ b/autograde/grade_grouped.py @@ -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() diff --git a/autograde/llm_grade.py b/autograde/llm_grade.py new file mode 100644 index 0000000..fdcd278 --- /dev/null +++ b/autograde/llm_grade.py @@ -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()