exam.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. from fastapi import APIRouter, Depends, Request
  2. from sqlalchemy.orm import Session
  3. from pydantic import BaseModel, Field
  4. from typing import Optional
  5. from database import get_db
  6. from models.chat import AIMessage
  7. from services.qwen_service import qwen_service
  8. router = APIRouter()
  9. class QuestionTypeItem(BaseModel):
  10. questionType: str = ""
  11. name: str = ""
  12. count: int = 0
  13. questionCount: int = 0
  14. scorePerQuestion: int = 0
  15. romanNumeral: str = ""
  16. class BuildPromptRequest(BaseModel):
  17. mode: str = ""
  18. client: str = ""
  19. projectType: str = ""
  20. examTitle: str = ""
  21. totalScore: int = 0
  22. questionTypes: list[QuestionTypeItem] = Field(default_factory=list)
  23. pptContent: str = ""
  24. @router.post("/exam/build_prompt")
  25. async def build_exam_prompt(
  26. request: Request,
  27. data: BuildPromptRequest,
  28. db: Session = Depends(get_db)
  29. ):
  30. """根据前端考试工坊参数生成提示词"""
  31. user = request.state.user
  32. if not user:
  33. return {"statusCode": 401, "msg": "未授权"}
  34. question_desc = []
  35. total_count = 0
  36. for item in data.questionTypes:
  37. count = item.count or item.questionCount or 0
  38. score = item.scorePerQuestion or 0
  39. qtype = item.questionType or item.name or "未命名题型"
  40. total_count += count
  41. question_desc.append(f"{qtype}{count}道,每道{score}分")
  42. question_text = ";".join(question_desc) if question_desc else "题型未提供"
  43. question_schema_lines = []
  44. for item in data.questionTypes:
  45. count = item.count or item.questionCount or 0
  46. score = item.scorePerQuestion or 0
  47. qtype = item.questionType or item.name or "未命名题型"
  48. if count <= 0:
  49. continue
  50. question_schema_lines.append(f"- {qtype}: {count}道,每道{score}分")
  51. question_schema = "\n".join(question_schema_lines) if question_schema_lines else "- 未提供有效题型"
  52. prompt = (
  53. "请根据以下要求直接生成一份完整试卷,并严格返回纯 JSON,不要输出 markdown 代码块、解释说明或额外文字。\n"
  54. f"生成模式:{data.mode or '未指定'}\n"
  55. f"客户端:{data.client or '未指定'}\n"
  56. f"项目类型:{data.projectType or '未指定'}\n"
  57. f"考试标题:{data.examTitle or '未命名考试'}\n"
  58. f"总分:{data.totalScore or 0}\n"
  59. f"总题量:{total_count}\n"
  60. f"题型要求:{question_text}\n"
  61. f"课件内容:{data.pptContent or '无'}\n"
  62. "请结合课件内容、工程类型和题型要求,生成有具体内容、具体选项、具体答案、具体解析的试卷。\n"
  63. "禁止输出“选项A”“题目1”这类占位内容,所有题目必须是可直接展示和作答的真实内容。\n"
  64. "JSON 输出结构必须符合以下格式:\n"
  65. "{\n"
  66. ' "title": "试卷标题",\n'
  67. ' "totalScore": 100,\n'
  68. ' "totalQuestions": 10,\n'
  69. ' "singleChoice": {"scorePerQuestion": 2, "totalScore": 20, "count": 10, "questions": [{"text": "题目内容", "options": [{"key": "A", "text": "具体选项内容"}, {"key": "B", "text": "具体选项内容"}, {"key": "C", "text": "具体选项内容"}, {"key": "D", "text": "具体选项内容"}], "answer": "A", "analysis": "解析内容"}]},\n'
  70. ' "judge": {"scorePerQuestion": 2, "totalScore": 0, "count": 0, "questions": [{"text": "题目内容", "answer": "正确", "analysis": "解析内容"}]},\n'
  71. ' "multiple": {"scorePerQuestion": 3, "totalScore": 0, "count": 0, "questions": [{"text": "题目内容", "options": [{"key": "A", "text": "具体选项内容"}, {"key": "B", "text": "具体选项内容"}, {"key": "C", "text": "具体选项内容"}, {"key": "D", "text": "具体选项内容"}], "answers": ["A", "C"], "analysis": "解析内容"}]},\n'
  72. ' "short": {"scorePerQuestion": 10, "totalScore": 0, "count": 0, "questions": [{"text": "题目内容", "outline": {"keyFactors": "答题要点", "measures": "参考措施"}}]}\n'
  73. "}\n"
  74. "请按下面的题型配置生成对应数量的题目,没有的题型 count 返回 0、questions 返回空数组:\n"
  75. f"{question_schema}"
  76. )
  77. return {
  78. "statusCode": 200,
  79. "msg": "success",
  80. "data": {"prompt": prompt}
  81. }
  82. class BuildSinglePromptRequest(BaseModel):
  83. question_type: str
  84. topic: str
  85. difficulty: str
  86. @router.post("/exam/build_single_prompt")
  87. async def build_single_question_prompt(
  88. request: Request,
  89. data: BuildSinglePromptRequest,
  90. db: Session = Depends(get_db)
  91. ):
  92. """生成单题提示词 - 对齐Go版本函数名"""
  93. user = request.state.user
  94. if not user:
  95. return {"statusCode": 401, "msg": "未授权"}
  96. prompt = f"""请生成1道关于{data.topic}的{data.question_type},难度为{data.difficulty}。"""
  97. return {
  98. "statusCode": 200,
  99. "msg": "success",
  100. "data": {"prompt": prompt}
  101. }
  102. class ModifyQuestionRequest(BaseModel):
  103. ai_conversation_id: int
  104. content: str
  105. @router.post("/re_modify_question")
  106. async def re_modify_question(
  107. request: Request,
  108. data: ModifyQuestionRequest,
  109. db: Session = Depends(get_db)
  110. ):
  111. """修改考试题目 - 实际修改ai_message表"""
  112. user = request.state.user
  113. if not user:
  114. return {"statusCode": 401, "msg": "未授权"}
  115. # 修改ai_message表中type='ai'的content
  116. result = db.query(AIMessage).filter(
  117. AIMessage.ai_conversation_id == data.ai_conversation_id,
  118. AIMessage.type == 'ai'
  119. ).update({"content": data.content})
  120. if result == 0:
  121. return {"statusCode": 404, "msg": "消息不存在"}
  122. db.commit()
  123. return {"statusCode": 200, "msg": "success"}
  124. class ReproduceSingleQuestionRequest(BaseModel):
  125. message: str = ""
  126. ai_conversation_id: Optional[int] = None
  127. regenerate_reason: str = ""
  128. @router.post("/re_produce_single_question")
  129. async def re_produce_single_question(
  130. request: Request,
  131. data: ReproduceSingleQuestionRequest,
  132. db: Session = Depends(get_db)
  133. ):
  134. """重新生成单题"""
  135. user = request.state.user
  136. if not user:
  137. return {"statusCode": 401, "msg": "未授权"}
  138. prompt = (data.message or "").strip()
  139. # 兼容旧版调用:未传 message 时,尝试根据会话和重生成原因构造提示词。
  140. if not prompt and data.ai_conversation_id:
  141. message = db.query(AIMessage).filter(
  142. AIMessage.ai_conversation_id == data.ai_conversation_id,
  143. AIMessage.type == 'ai'
  144. ).first()
  145. if not message:
  146. return {"statusCode": 404, "msg": "消息不存在"}
  147. prompt = (message.content or "").strip()
  148. if data.regenerate_reason:
  149. prompt = f"{prompt}\n\n请根据以下要求重新生成:{data.regenerate_reason}"
  150. if not prompt:
  151. return {"statusCode": 400, "msg": "缺少生成内容"}
  152. try:
  153. new_question = await qwen_service.chat([
  154. {"role": "user", "content": prompt}
  155. ])
  156. except Exception as e:
  157. return {"statusCode": 500, "msg": f"AI生成失败: {str(e)}"}
  158. return {
  159. "statusCode": 200,
  160. "msg": "success",
  161. "data": {
  162. "ai_conversation_id": data.ai_conversation_id,
  163. "new_question": new_question,
  164. "reply": new_question,
  165. "content": new_question,
  166. "message": new_question
  167. }
  168. }