admin_knowledge.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. from typing import Optional
  2. import json
  3. import csv
  4. from io import StringIO
  5. from datetime import datetime, timezone
  6. from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File
  7. from pydantic import BaseModel, Field
  8. from app.core.security import get_current_user
  9. from app.core.config import get_settings
  10. settings = get_settings()
  11. router = APIRouter(prefix="/admin/knowledge", tags=["管理-知识库"])
  12. # ============================================
  13. # Schemas
  14. # ============================================
  15. class DrugCreate(BaseModel):
  16. drug_id: str
  17. name: str
  18. name_en: Optional[str] = None
  19. pinyin: Optional[str] = None
  20. category: Optional[str] = None
  21. subcategory: Optional[str] = None
  22. approval_number: Optional[str] = None
  23. sections: Optional[dict] = None
  24. source_version: Optional[str] = None
  25. source_volume: Optional[str] = None
  26. source_page: Optional[str] = None
  27. class DrugUpdate(BaseModel):
  28. name: Optional[str] = None
  29. name_en: Optional[str] = None
  30. pinyin: Optional[str] = None
  31. category: Optional[str] = None
  32. subcategory: Optional[str] = None
  33. approval_number: Optional[str] = None
  34. sections: Optional[dict] = None
  35. source_version: Optional[str] = None
  36. source_volume: Optional[str] = None
  37. source_page: Optional[str] = None
  38. is_active: Optional[bool] = None
  39. class KnowledgePointCreate(BaseModel):
  40. point_id: str
  41. subject: str
  42. chapter_id: str
  43. chapter_name: str
  44. title: str
  45. content: str
  46. difficulty: int = Field(default=1, ge=1, le=5)
  47. frequency: Optional[str] = None
  48. related_drugs: Optional[list] = None
  49. source: Optional[str] = None
  50. class KnowledgePointUpdate(BaseModel):
  51. chapter_name: Optional[str] = None
  52. title: Optional[str] = None
  53. content: Optional[str] = None
  54. difficulty: Optional[int] = Field(default=None, ge=1, le=5)
  55. frequency: Optional[str] = None
  56. related_drugs: Optional[list] = None
  57. source: Optional[str] = None
  58. class QuestionCreate(BaseModel):
  59. question_id: str
  60. question_type: str
  61. subject: str
  62. chapter_id: str
  63. difficulty: int = Field(default=1, ge=1, le=5)
  64. content: str
  65. options: list[str]
  66. answer: str
  67. explanation: str
  68. knowledge_point_ids: Optional[list[str]] = None
  69. source: Optional[str] = None
  70. frequency: Optional[str] = None
  71. class QuestionUpdate(BaseModel):
  72. question_type: Optional[str] = None
  73. subject: Optional[str] = None
  74. chapter_id: Optional[str] = None
  75. difficulty: Optional[int] = Field(default=None, ge=1, le=5)
  76. content: Optional[str] = None
  77. options: Optional[list[str]] = None
  78. answer: Optional[str] = None
  79. explanation: Optional[str] = None
  80. knowledge_point_ids: Optional[list[str]] = None
  81. source: Optional[str] = None
  82. frequency: Optional[str] = None
  83. audited: Optional[bool] = None
  84. class ReindexRequest(BaseModel):
  85. collection: str = Field(default="drug_entries", description="drug_entries / exam_knowledge")
  86. drug_ids: Optional[list[str]] = Field(default=None, description="指定药品ID,不填则全量重建")
  87. class ImportRequest(BaseModel):
  88. type: str = Field(..., description="drug / knowledge_point / question")
  89. format: str = Field(default="json", description="json / csv")
  90. data: str = Field(..., description="JSON/CSV 内容字符串")
  91. class BatchAuditRequest(BaseModel):
  92. question_ids: list[str]
  93. audited: bool = True
  94. # ============================================
  95. # 药品管理 CRUD
  96. # ============================================
  97. @router.get("/drugs")
  98. async def list_drugs(
  99. keyword: Optional[str] = Query(None, description="药品名称搜索"),
  100. category: Optional[str] = Query(None, description="药品分类"),
  101. page: int = Query(1, ge=1),
  102. page_size: int = Query(20, ge=1, le=100),
  103. user: dict = Depends(get_current_user),
  104. ):
  105. return {
  106. "items": [],
  107. "total": 0,
  108. "page": page,
  109. "page_size": page_size,
  110. "message": "数据入库后可用",
  111. }
  112. @router.get("/drugs/{drug_id}")
  113. async def get_drug(drug_id: str, user: dict = Depends(get_current_user)):
  114. return {"message": "数据入库后可用", "drug_id": drug_id}
  115. @router.post("/drugs")
  116. async def create_drug(
  117. drug: DrugCreate,
  118. user: dict = Depends(get_current_user),
  119. ):
  120. return {"message": "已创建", "drug_id": drug.drug_id}
  121. @router.put("/drugs/{drug_id}")
  122. async def update_drug(
  123. drug_id: str,
  124. drug: DrugUpdate,
  125. user: dict = Depends(get_current_user),
  126. ):
  127. return {"message": "已更新", "drug_id": drug_id}
  128. @router.delete("/drugs/{drug_id}")
  129. async def delete_drug(drug_id: str, user: dict = Depends(get_current_user)):
  130. return {"message": "已标记删除", "drug_id": drug_id}
  131. @router.post("/drugs/import")
  132. async def import_drugs(
  133. req: ImportRequest,
  134. user: dict = Depends(get_current_user),
  135. ):
  136. count = 0
  137. if req.format == "json":
  138. try:
  139. items = json.loads(req.data)
  140. count = len(items) if isinstance(items, list) else 0
  141. except json.JSONDecodeError as e:
  142. raise HTTPException(status_code=400, detail=f"JSON 解析失败: {str(e)}")
  143. elif req.format == "csv":
  144. try:
  145. reader = csv.DictReader(StringIO(req.data))
  146. count = sum(1 for _ in reader)
  147. except Exception as e:
  148. raise HTTPException(status_code=400, detail=f"CSV 解析失败: {str(e)}")
  149. return {
  150. "message": "导入任务已提交",
  151. "total": count,
  152. "success": 0,
  153. "failed": 0,
  154. "note": "Phase 2 实现异步批量写入 + 向量化",
  155. }
  156. @router.post("/drugs/reindex")
  157. async def reindex_drugs(
  158. req: ReindexRequest,
  159. user: dict = Depends(get_current_user),
  160. ):
  161. return {
  162. "message": "重建索引任务已提交",
  163. "collection": req.collection,
  164. "drug_ids": req.drug_ids,
  165. "status": "pending",
  166. "note": "Phase 2 实现:重新 chunk → 向量化 → 写入 Milvus",
  167. }
  168. # ============================================
  169. # 知识点管理 CRUD
  170. # ============================================
  171. @router.get("/knowledge-points")
  172. async def list_knowledge_points(
  173. subject: Optional[str] = Query(None),
  174. chapter_id: Optional[str] = Query(None),
  175. difficulty: Optional[int] = Query(None, ge=1, le=5),
  176. frequency: Optional[str] = Query(None),
  177. page: int = Query(1, ge=1),
  178. page_size: int = Query(20, ge=1, le=100),
  179. user: dict = Depends(get_current_user),
  180. ):
  181. return {
  182. "items": [],
  183. "total": 0,
  184. "page": page,
  185. "page_size": page_size,
  186. }
  187. @router.get("/knowledge-points/{point_id}")
  188. async def get_knowledge_point(point_id: str, user: dict = Depends(get_current_user)):
  189. return {"message": "数据入库后可用", "point_id": point_id}
  190. @router.post("/knowledge-points")
  191. async def create_knowledge_point(
  192. kp: KnowledgePointCreate,
  193. user: dict = Depends(get_current_user),
  194. ):
  195. return {"message": "已创建", "point_id": kp.point_id}
  196. @router.put("/knowledge-points/{point_id}")
  197. async def update_knowledge_point(
  198. point_id: str,
  199. kp: KnowledgePointUpdate,
  200. user: dict = Depends(get_current_user),
  201. ):
  202. return {"message": "已更新", "point_id": point_id}
  203. @router.delete("/knowledge-points/{point_id}")
  204. async def delete_knowledge_point(point_id: str, user: dict = Depends(get_current_user)):
  205. return {"message": "已删除", "point_id": point_id}
  206. @router.post("/knowledge-points/import")
  207. async def import_knowledge_points(
  208. req: ImportRequest,
  209. user: dict = Depends(get_current_user),
  210. ):
  211. count = 0
  212. try:
  213. items = json.loads(req.data)
  214. count = len(items) if isinstance(items, list) else 0
  215. except json.JSONDecodeError as e:
  216. raise HTTPException(status_code=400, detail=f"JSON 解析失败: {str(e)}")
  217. return {
  218. "message": "导入任务已提交",
  219. "total": count,
  220. "success": 0,
  221. "failed": 0,
  222. }
  223. # ============================================
  224. # 题库管理 CRUD
  225. # ============================================
  226. @router.get("/questions")
  227. async def list_questions(
  228. subject: Optional[str] = Query(None),
  229. chapter_id: Optional[str] = Query(None),
  230. question_type: Optional[str] = Query(None),
  231. difficulty: Optional[int] = Query(None, ge=1, le=5),
  232. audited: Optional[bool] = Query(None),
  233. page: int = Query(1, ge=1),
  234. page_size: int = Query(20, ge=1, le=100),
  235. user: dict = Depends(get_current_user),
  236. ):
  237. return {
  238. "items": [],
  239. "total": 0,
  240. "page": page,
  241. "page_size": page_size,
  242. }
  243. @router.get("/questions/{question_id}")
  244. async def get_question(question_id: str, user: dict = Depends(get_current_user)):
  245. return {"message": "数据入库后可用", "question_id": question_id}
  246. @router.post("/questions")
  247. async def create_question(
  248. q: QuestionCreate,
  249. user: dict = Depends(get_current_user),
  250. ):
  251. return {"message": "已创建", "question_id": q.question_id}
  252. @router.put("/questions/{question_id}")
  253. async def update_question(
  254. question_id: str,
  255. q: QuestionUpdate,
  256. user: dict = Depends(get_current_user),
  257. ):
  258. return {"message": "已更新", "question_id": question_id}
  259. @router.delete("/questions/{question_id}")
  260. async def delete_question(question_id: str, user: dict = Depends(get_current_user)):
  261. return {"message": "已删除", "question_id": question_id}
  262. @router.post("/questions/batch-audit")
  263. async def batch_audit_questions(
  264. req: BatchAuditRequest,
  265. user: dict = Depends(get_current_user),
  266. ):
  267. return {
  268. "message": "批量审核完成",
  269. "question_ids": req.question_ids,
  270. "audited": req.audited,
  271. }
  272. @router.post("/questions/import")
  273. async def import_questions(
  274. req: ImportRequest,
  275. user: dict = Depends(get_current_user),
  276. ):
  277. count = 0
  278. try:
  279. items = json.loads(req.data)
  280. count = len(items) if isinstance(items, list) else 0
  281. except json.JSONDecodeError as e:
  282. raise HTTPException(status_code=400, detail=f"JSON 解析失败: {str(e)}")
  283. return {
  284. "message": "导入任务已提交",
  285. "total": count,
  286. "success": 0,
  287. "failed": 0,
  288. }
  289. @router.post("/questions/generate")
  290. async def ai_generate_questions(
  291. point_id: str = Query(..., description="知识点ID"),
  292. count: int = Query(10, ge=1, le=50),
  293. question_type: str = Query("A", description="A/B/X"),
  294. user: dict = Depends(get_current_user),
  295. ):
  296. return {
  297. "message": "AI 出题任务已提交",
  298. "point_id": point_id,
  299. "count": count,
  300. "question_type": question_type,
  301. "status": "pending",
  302. "note": "Phase 2 实现:LLM 根据知识点生成题目",
  303. }
  304. # ============================================
  305. # 知识库状态与统计
  306. # ============================================
  307. @router.get("/stats")
  308. async def knowledge_stats(user: dict = Depends(get_current_user)):
  309. return {
  310. "drugs": {"total": 0, "active": 0, "with_chunks": 0},
  311. "knowledge_points": {"total": 0, "by_subject": {}},
  312. "questions": {"total": 0, "audited": 0, "pending": 0},
  313. "vector_index": {
  314. "status": "empty",
  315. "collection": settings.milvus_collection,
  316. "last_reindex": None,
  317. },
  318. "last_data_update": None,
  319. }