knowledge.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. from __future__ import annotations
  2. import json
  3. from fastapi import APIRouter, Depends, Query, WebSocket, WebSocketDisconnect
  4. from fastapi.responses import StreamingResponse
  5. from sqlalchemy.orm import Session
  6. from app.db.repositories import knowledge as knowledge_repo
  7. from app.db.repositories.base import create_model
  8. from app.db.session import get_db
  9. from app.schemas.common import Page
  10. from app.schemas.knowledge import (
  11. KnowledgeAskRequest,
  12. KnowledgeAskResponse,
  13. KnowledgeCatalogResponse,
  14. KnowledgeGraphResponse,
  15. KnowledgeItemCreate,
  16. KnowledgeItemRead,
  17. KnowledgeNoteDetail,
  18. KnowledgeOverviewResponse,
  19. KnowledgeSearchHit,
  20. KnowledgeSearchRequest,
  21. KnowledgeSearchResponse,
  22. )
  23. from app.services.knowledge_hub_service import (
  24. build_answer,
  25. build_catalog,
  26. build_graph,
  27. build_note_detail,
  28. build_overview,
  29. stream_answer,
  30. )
  31. from app.services.knowledge_service import search_knowledge
  32. router = APIRouter(prefix="/knowledge", tags=["知识中心"])
  33. @router.get("", response_model=Page[KnowledgeItemRead], summary="知识条目列表")
  34. def list_items(
  35. mine_id: str | None = Query(None),
  36. knowledge_type: str | None = Query(None),
  37. category: str | None = Query(None),
  38. keyword: str | None = Query(None),
  39. page: int = Query(1, ge=1),
  40. page_size: int = Query(20, ge=1, le=200),
  41. db: Session = Depends(get_db),
  42. ):
  43. items, total = knowledge_repo.list_knowledge(
  44. db,
  45. mine_id=mine_id,
  46. knowledge_type=knowledge_type,
  47. category=category,
  48. keyword=keyword,
  49. page=page,
  50. page_size=page_size,
  51. )
  52. return Page(items=items, total=total, page=page, page_size=page_size)
  53. @router.post("", response_model=KnowledgeItemRead, status_code=201, summary="新增知识")
  54. def create_knowledge(payload: KnowledgeItemCreate, db: Session = Depends(get_db)):
  55. data = payload.model_dump(exclude={"metadata"})
  56. data["metadata_json"] = payload.metadata
  57. return create_model(db, knowledge_repo.KnowledgeItem, data)
  58. @router.post(
  59. "/search",
  60. response_model=KnowledgeSearchResponse,
  61. summary="知识检索",
  62. )
  63. def search(payload: KnowledgeSearchRequest, db: Session = Depends(get_db)):
  64. hits = search_knowledge(db, payload)
  65. return KnowledgeSearchResponse(
  66. query=payload.query,
  67. total=len(hits),
  68. items=[
  69. KnowledgeSearchHit(
  70. item=KnowledgeItemRead.model_validate(hit["item"]),
  71. score=hit["score"],
  72. highlight=hit["highlight"],
  73. )
  74. for hit in hits
  75. ],
  76. )
  77. @router.get(
  78. "/overview",
  79. response_model=KnowledgeOverviewResponse,
  80. summary="知识中心总览",
  81. )
  82. async def overview() -> KnowledgeOverviewResponse:
  83. return await build_overview()
  84. @router.get(
  85. "/catalog",
  86. response_model=KnowledgeCatalogResponse,
  87. summary="知识中心目录",
  88. )
  89. async def catalog(
  90. page: int = Query(1, ge=1),
  91. page_size: int = Query(20, ge=1, le=200),
  92. category: str | None = Query(None),
  93. keyword: str | None = Query(None),
  94. tag: str | None = Query(None),
  95. ) -> KnowledgeCatalogResponse:
  96. return await build_catalog(
  97. page=page,
  98. page_size=page_size,
  99. category=category,
  100. keyword=keyword,
  101. tag=tag,
  102. )
  103. @router.get(
  104. "/catalog/{note_id:path}",
  105. response_model=KnowledgeNoteDetail,
  106. summary="知识条目详情",
  107. )
  108. async def note_detail(note_id: str) -> KnowledgeNoteDetail:
  109. return await build_note_detail(note_id)
  110. @router.post(
  111. "/ask",
  112. response_model=KnowledgeAskResponse,
  113. summary="知识检索问答",
  114. )
  115. async def ask(payload: KnowledgeAskRequest, db: Session = Depends(get_db)) -> KnowledgeAskResponse:
  116. return await build_answer(db, payload)
  117. @router.post(
  118. "/rag",
  119. response_model=KnowledgeAskResponse,
  120. summary="RAG 知识问答",
  121. )
  122. async def rag(payload: KnowledgeAskRequest, db: Session = Depends(get_db)) -> KnowledgeAskResponse:
  123. rag_payload = payload.model_copy(update={"generate_answer": True})
  124. return await build_answer(db, rag_payload)
  125. @router.post(
  126. "/rag/stream",
  127. summary="RAG 知识问答流式接口",
  128. )
  129. async def rag_stream(payload: KnowledgeAskRequest, db: Session = Depends(get_db)):
  130. return _rag_stream_response(payload, db)
  131. @router.get(
  132. "/rag/stream",
  133. summary="RAG 知识问答流式接口(EventSource)",
  134. )
  135. async def rag_stream_get(
  136. question: str = Query(min_length=1),
  137. top_k: int = Query(5, ge=1, le=20),
  138. db: Session = Depends(get_db),
  139. ):
  140. payload = KnowledgeAskRequest(
  141. question=question,
  142. top_k=top_k,
  143. generate_answer=True,
  144. )
  145. return _rag_stream_response(payload, db)
  146. def _rag_stream_response(payload: KnowledgeAskRequest, db: Session):
  147. async def event_stream():
  148. async for item in stream_answer(db, payload):
  149. data = json.dumps(item["data"], ensure_ascii=False)
  150. yield f"event: {item['event']}\ndata: {data}\n\n"
  151. return StreamingResponse(
  152. event_stream(),
  153. media_type="text/event-stream",
  154. headers={
  155. "Cache-Control": "no-cache",
  156. "Connection": "keep-alive",
  157. "X-Accel-Buffering": "no",
  158. },
  159. )
  160. @router.websocket("/rag/ws")
  161. async def rag_ws(websocket: WebSocket, db: Session = Depends(get_db)):
  162. await websocket.accept()
  163. try:
  164. payload = KnowledgeAskRequest.model_validate(await websocket.receive_json())
  165. async for item in stream_answer(db, payload):
  166. await websocket.send_json(item)
  167. except WebSocketDisconnect:
  168. return
  169. except Exception as exc:
  170. await websocket.send_json(
  171. {
  172. "event": "error",
  173. "data": {
  174. "message": f"流式问答失败:{type(exc).__name__}",
  175. },
  176. }
  177. )
  178. finally:
  179. try:
  180. await websocket.close()
  181. except RuntimeError:
  182. pass
  183. @router.get(
  184. "/graph",
  185. response_model=KnowledgeGraphResponse,
  186. summary="风险关联知识图谱",
  187. )
  188. async def graph(topic: str = Query("瓦斯超限风险事件")) -> KnowledgeGraphResponse:
  189. return await build_graph(topic)