| import json |
| import re |
|
|
| from fastapi import APIRouter |
| from pydantic import BaseModel |
|
|
| from app.gpt.gpt_factory import GPTFactory |
| from app.gpt.utils import strip_think_blocks |
| from app.models.model_config import ModelConfig |
| from app.services.provider import ProviderService |
| from app.utils.logger import get_logger |
| from app.utils.response import ResponseWrapper as R |
|
|
| logger = get_logger(__name__) |
|
|
| router = APIRouter() |
|
|
|
|
| class FlashcardRequest(BaseModel): |
| content: str |
| provider_id: str |
| model_name: str |
| count: int = 10 |
|
|
|
|
| SYSTEM_PROMPT = """你是一个学习卡片生成助手。请根据用户提供的笔记内容,提炼关键知识点,生成问答式记忆闪卡。 |
| |
| 要求: |
| - 每张卡片包含 front(问题/正面)和 back(答案/背面) |
| - 问题应聚焦核心概念、定义、关键结论,便于主动回忆 |
| - 答案简洁准确,控制在 1~3 句话 |
| - 最多生成 {count} 张,不要硬凑,宁缺毋滥 |
| - 严格只输出 JSON 数组,不要任何额外说明或 markdown 代码块,格式: |
| [{{"front": "问题", "back": "答案"}}]""" |
|
|
|
|
| def _parse_cards(text: str) -> list[dict]: |
| """从 LLM 输出中解析出卡片数组,容忍代码块包裹。""" |
| cleaned = text.strip() |
| |
| cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", cleaned, flags=re.IGNORECASE) |
| try: |
| data = json.loads(cleaned) |
| except json.JSONDecodeError: |
| |
| match = re.search(r"\[.*\]", cleaned, flags=re.DOTALL) |
| if not match: |
| return [] |
| try: |
| data = json.loads(match.group(0)) |
| except json.JSONDecodeError: |
| return [] |
|
|
| cards = [] |
| for item in data if isinstance(data, list) else []: |
| front = (item or {}).get("front") |
| back = (item or {}).get("back") |
| if front and back: |
| cards.append({"front": str(front), "back": str(back)}) |
| return cards |
|
|
|
|
| @router.post("/flashcards/generate") |
| def generate_flashcards(data: FlashcardRequest): |
| """根据笔记内容用 LLM 生成问答闪卡。""" |
| content = data.content.strip() |
| if not content: |
| return R.error(msg="笔记内容为空,无法生成闪卡") |
|
|
| provider = ProviderService.get_provider_by_id(data.provider_id) |
| if not provider: |
| return R.error(msg=f"未找到模型供应商: {data.provider_id}") |
|
|
| config = ModelConfig( |
| api_key=provider["api_key"], |
| base_url=provider["base_url"], |
| model_name=data.model_name, |
| provider=provider["type"], |
| name=provider["name"], |
| ) |
| gpt = GPTFactory.from_config(config) |
|
|
| |
| max_chars = 12000 |
| snippet = content[:max_chars] |
|
|
| try: |
| response = gpt.client.chat.completions.create( |
| model=gpt.model, |
| messages=[ |
| {"role": "system", "content": SYSTEM_PROMPT.format(count=data.count)}, |
| {"role": "user", "content": snippet}, |
| ], |
| temperature=0.4, |
| ) |
| raw = strip_think_blocks(response.choices[0].message.content) |
| cards = _parse_cards(raw) |
| if not cards: |
| logger.warning(f"闪卡解析为空,原始输出: {raw[:200]}") |
| return R.error(msg="未能生成有效闪卡,请重试") |
| return R.success(data={"cards": cards}) |
| except Exception as e: |
| logger.error(f"生成闪卡失败: {e}", exc_info=True) |
| return R.error(msg=f"生成闪卡失败: {str(e)}") |
|
|