routes.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. from datetime import datetime
  2. from typing import TypeVar
  3. from core_domain import ServiceHealth
  4. from core_shared import error_detail
  5. from fastapi import APIRouter, Depends, HTTPException
  6. from sqlalchemy import text
  7. from sqlalchemy.orm import Session
  8. from app.application.document_parsers import DocumentParseError
  9. from app.application.services import (
  10. KnowledgeApplicationService,
  11. KnowledgeIndexingError,
  12. build_knowledge_application_service,
  13. )
  14. from app.bootstrap.settings import KnowledgeServiceSettings
  15. from app.db.session import get_db
  16. from app.infrastructure.object_storage import ObjectStorageError
  17. from app.schemas.knowledge import (
  18. ApiResponse,
  19. DeleteData,
  20. KnowledgeBaseCreateRequestDto,
  21. KnowledgeBaseDeleteRequestDto,
  22. KnowledgeBaseDetailRequestDto,
  23. KnowledgeBaseDto,
  24. KnowledgeBaseListRequestDto,
  25. KnowledgeBaseStatusRequestDto,
  26. KnowledgeBaseStatusUpdateRequest,
  27. KnowledgeBaseUpdateRequestDto,
  28. KnowledgeBaseReindexData,
  29. KnowledgeBaseReindexRequestDto,
  30. KnowledgeChunkDetailRequestDto,
  31. KnowledgeChunkDeleteRequestDto,
  32. KnowledgeChunkDto,
  33. KnowledgeChunkListRequestDto,
  34. KnowledgeDocumentCreateRequestDto,
  35. KnowledgeDocumentContentData,
  36. KnowledgeDocumentContentRequestDto,
  37. KnowledgeDocumentDeleteRequestDto,
  38. KnowledgeDocumentDetailRequestDto,
  39. KnowledgeDocumentDto,
  40. KnowledgeDocumentIngestData,
  41. KnowledgeDocumentListRequestDto,
  42. KnowledgeDocumentParseRequest,
  43. KnowledgeDocumentParseRequestDto,
  44. KnowledgeDocumentParseData,
  45. KnowledgeDocumentReindexRequestDto,
  46. KnowledgeDocumentStorageStatusData,
  47. KnowledgeDocumentStorageStatusRequestDto,
  48. KnowledgeDocumentStatusRequestDto,
  49. KnowledgeDocumentUpdateRequestDto,
  50. KnowledgeIndexJobData,
  51. KnowledgeIndexJobDetailRequestDto,
  52. KnowledgeIndexJobListRequestDto,
  53. KnowledgeIndexJobRetryRequestDto,
  54. KnowledgeSearchRequestDto,
  55. KnowledgeSearchRequest,
  56. KnowledgeSearchResultDto,
  57. KnowledgeSettingsDto,
  58. KnowledgeSettingsUpdateRequestDto,
  59. KnowledgeStorageHealthData,
  60. KnowledgeStorageHealthRequestDto,
  61. PageResult,
  62. )
  63. router = APIRouter()
  64. T = TypeVar("T")
  65. def ok(data: T) -> ApiResponse[T]:
  66. return ApiResponse[T](
  67. data=data,
  68. requestId="",
  69. serverTime=datetime.utcnow())
  70. def paginate(items: list[T], *, page: int, page_size: int) -> PageResult[T]:
  71. start = (page - 1) * page_size
  72. return PageResult.from_items(
  73. items=items[start:start + page_size],
  74. total=len(items),
  75. page=page,
  76. page_size=page_size)
  77. def get_knowledge_settings() -> KnowledgeServiceSettings:
  78. return KnowledgeServiceSettings()
  79. def get_knowledge_application_service(
  80. db: Session = Depends(get_db),
  81. settings: KnowledgeServiceSettings = Depends(get_knowledge_settings)) -> KnowledgeApplicationService:
  82. return build_knowledge_application_service(db=db, settings=settings)
  83. @router.get("/health", response_model=ServiceHealth)
  84. def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
  85. db.execute(text("SELECT 1"))
  86. return ServiceHealth(service="knowledge-service", status="ok", database="ok")
  87. @router.post("/storage/health", response_model=ApiResponse[KnowledgeStorageHealthData])
  88. def storage_health_contract(
  89. payload: KnowledgeStorageHealthRequestDto,
  90. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeStorageHealthData]:
  91. try:
  92. health = service.read_storage_health()
  93. except ObjectStorageError as exc:
  94. health = {
  95. "backend": service.settings.object_storage_backend,
  96. "bucket": service.settings.object_storage_bucket,
  97. "available": False,
  98. "message": str(exc),
  99. }
  100. return ok(KnowledgeStorageHealthData(
  101. backend=str(health.get("backend") or ""),
  102. bucket=str(health.get("bucket") or ""),
  103. available=bool(health.get("available")),
  104. message=str(health["message"]) if health.get("message") is not None else None,
  105. checkedTime=datetime.utcnow()))
  106. @router.post("/bases/list", response_model=ApiResponse[PageResult[KnowledgeBaseDto]])
  107. def list_bases_contract(
  108. payload: KnowledgeBaseListRequestDto,
  109. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[PageResult[KnowledgeBaseDto]]:
  110. items = [
  111. KnowledgeBaseDto.from_entity(item)
  112. for item in service.list_bases_filtered(
  113. keyword=payload.keyword,
  114. status=payload.status)
  115. ]
  116. return ok(paginate(items, page=payload.page, page_size=payload.pageSize))
  117. @router.post("/bases/create", response_model=ApiResponse[KnowledgeBaseDto])
  118. def create_base_contract(
  119. payload: KnowledgeBaseCreateRequestDto,
  120. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseDto]:
  121. return ok(KnowledgeBaseDto.from_entity(service.create_base_from_contract(payload)))
  122. @router.post("/bases/detail", response_model=ApiResponse[KnowledgeBaseDto])
  123. def detail_base_contract(
  124. payload: KnowledgeBaseDetailRequestDto,
  125. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseDto]:
  126. entity = service.base_repository.get_by_id(knowledge_base_id=payload.knowledgeBaseId)
  127. if entity is None:
  128. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_base.not_found", id=payload.knowledgeBaseId))
  129. return ok(KnowledgeBaseDto.from_entity(entity))
  130. @router.post("/bases/update", response_model=ApiResponse[KnowledgeBaseDto])
  131. def update_base_contract(
  132. payload: KnowledgeBaseUpdateRequestDto,
  133. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseDto]:
  134. entity = service.update_base_from_contract(payload)
  135. if entity is None:
  136. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_base.not_found", id=payload.knowledgeBaseId))
  137. return ok(KnowledgeBaseDto.from_entity(entity))
  138. @router.post("/bases/status", response_model=ApiResponse[KnowledgeBaseDto])
  139. def update_base_status_contract(
  140. payload: KnowledgeBaseStatusRequestDto,
  141. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseDto]:
  142. entity = service.update_base_status(
  143. knowledge_base_id=payload.knowledgeBaseId,
  144. payload=KnowledgeBaseStatusUpdateRequest(status=payload.status))
  145. if entity is None:
  146. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_base.not_found", id=payload.knowledgeBaseId))
  147. return ok(KnowledgeBaseDto.from_entity(entity))
  148. @router.post("/bases/delete", response_model=ApiResponse[DeleteData])
  149. def delete_base_contract(
  150. payload: KnowledgeBaseDeleteRequestDto,
  151. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[DeleteData]:
  152. try:
  153. deleted = service.delete_base(knowledge_base_id=payload.knowledgeBaseId)
  154. except ObjectStorageError as exc:
  155. raise HTTPException(status_code=503, detail=error_detail("error.storage.unavailable", message=str(exc))) from exc
  156. return ok(DeleteData(
  157. deleted=deleted,
  158. knowledgeBaseId=payload.knowledgeBaseId))
  159. @router.post("/documents/list", response_model=ApiResponse[PageResult[KnowledgeDocumentDto]])
  160. def list_documents_contract(
  161. payload: KnowledgeDocumentListRequestDto,
  162. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[PageResult[KnowledgeDocumentDto]]:
  163. items = [
  164. KnowledgeDocumentDto.from_entity(item)
  165. for item in service.list_documents_filtered(
  166. knowledge_base_id=payload.knowledgeBaseId,
  167. keyword=payload.keyword,
  168. status=payload.status,
  169. source_type=payload.sourceType)
  170. ]
  171. return ok(paginate(items, page=payload.page, page_size=payload.pageSize))
  172. @router.post("/documents/create", response_model=ApiResponse[KnowledgeDocumentIngestData])
  173. def create_document_contract(
  174. payload: KnowledgeDocumentCreateRequestDto,
  175. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentIngestData]:
  176. try:
  177. result = service.create_document_from_contract_result(payload)
  178. except KnowledgeIndexingError as exc:
  179. raise HTTPException(status_code=503, detail=error_detail("error.knowledge.indexing_failed", message=str(exc))) from exc
  180. except ObjectStorageError as exc:
  181. raise HTTPException(status_code=503, detail=error_detail("error.storage.unavailable", message=str(exc))) from exc
  182. except ValueError as exc:
  183. raise HTTPException(status_code=422, detail=error_detail("error.validation", message=str(exc))) from exc
  184. return ok(KnowledgeDocumentIngestData(
  185. document=KnowledgeDocumentDto.from_entity(result.document),
  186. chunks=[KnowledgeChunkDto.from_entity(item) for item in result.chunks],
  187. queued=result.queued,
  188. job=result.job))
  189. @router.post("/documents/detail", response_model=ApiResponse[KnowledgeDocumentDto])
  190. def detail_document_contract(
  191. payload: KnowledgeDocumentDetailRequestDto,
  192. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentDto]:
  193. entity = service.document_repository.get_by_id(document_id=payload.documentId)
  194. if entity is None:
  195. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_document.not_found", id=payload.documentId))
  196. return ok(KnowledgeDocumentDto.from_entity(entity))
  197. @router.post("/documents/update", response_model=ApiResponse[KnowledgeDocumentDto])
  198. def update_document_contract(
  199. payload: KnowledgeDocumentUpdateRequestDto,
  200. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentDto]:
  201. entity = service.update_document_from_contract(payload)
  202. if entity is None:
  203. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_document.not_found", id=payload.documentId))
  204. return ok(KnowledgeDocumentDto.from_entity(entity))
  205. @router.post("/documents/status", response_model=ApiResponse[KnowledgeDocumentDto])
  206. def update_document_status_contract(
  207. payload: KnowledgeDocumentStatusRequestDto,
  208. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentDto]:
  209. entity = service.document_repository.update_status(
  210. document_id=payload.documentId,
  211. status=payload.status)
  212. if entity is None:
  213. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_document.not_found", id=payload.documentId))
  214. return ok(KnowledgeDocumentDto.from_entity(entity))
  215. @router.post("/documents/delete", response_model=ApiResponse[DeleteData])
  216. def delete_document_contract(
  217. payload: KnowledgeDocumentDeleteRequestDto,
  218. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[DeleteData]:
  219. try:
  220. result = service.delete_document_result(document_id=payload.documentId)
  221. except ObjectStorageError as exc:
  222. raise HTTPException(status_code=503, detail=error_detail("error.storage.unavailable", message=str(exc))) from exc
  223. return ok(DeleteData(
  224. deleted=bool(result.get("deleted")),
  225. documentId=payload.documentId,
  226. objectDeleted=bool(result.get("objectDeleted"))))
  227. @router.post("/documents/reindex", response_model=ApiResponse[KnowledgeDocumentIngestData])
  228. def reindex_document_contract(
  229. payload: KnowledgeDocumentReindexRequestDto,
  230. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentIngestData]:
  231. try:
  232. result = service.reindex_document_from_contract_result(payload)
  233. except KnowledgeIndexingError as exc:
  234. raise HTTPException(status_code=503, detail=error_detail("error.knowledge.indexing_failed", message=str(exc))) from exc
  235. except ObjectStorageError as exc:
  236. raise HTTPException(status_code=503, detail=error_detail("error.storage.unavailable", message=str(exc))) from exc
  237. except ValueError as exc:
  238. raise HTTPException(status_code=422, detail=error_detail("error.validation", message=str(exc))) from exc
  239. if result is None:
  240. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_document.not_found", id=payload.documentId))
  241. return ok(KnowledgeDocumentIngestData(
  242. document=KnowledgeDocumentDto.from_entity(result.document),
  243. chunks=[KnowledgeChunkDto.from_entity(item) for item in result.chunks],
  244. queued=result.queued,
  245. job=result.job))
  246. @router.post("/jobs/list", response_model=ApiResponse[PageResult[KnowledgeIndexJobData]])
  247. def list_index_jobs_contract(
  248. payload: KnowledgeIndexJobListRequestDto,
  249. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[PageResult[KnowledgeIndexJobData]]:
  250. items = service.list_index_jobs(
  251. knowledge_base_id=payload.knowledgeBaseId,
  252. document_id=payload.documentId,
  253. status=payload.status)
  254. return ok(paginate(items, page=payload.page, page_size=payload.pageSize))
  255. @router.post("/jobs/detail", response_model=ApiResponse[KnowledgeIndexJobData])
  256. def detail_index_job_contract(
  257. payload: KnowledgeIndexJobDetailRequestDto,
  258. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeIndexJobData]:
  259. job = service.detail_index_job(document_id=payload.documentId)
  260. if job is None:
  261. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_index_job.not_found", id=payload.documentId))
  262. return ok(job)
  263. @router.post("/jobs/retry", response_model=ApiResponse[KnowledgeDocumentIngestData])
  264. def retry_index_job_contract(
  265. payload: KnowledgeIndexJobRetryRequestDto,
  266. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentIngestData]:
  267. result = service.reindex_document_from_contract_result(
  268. KnowledgeDocumentReindexRequestDto(
  269. documentId=payload.documentId,
  270. chunkSize=payload.chunkSize,
  271. chunkOverlap=payload.chunkOverlap,
  272. asyncMode=True))
  273. if result is None:
  274. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_document.not_found", id=payload.documentId))
  275. return ok(KnowledgeDocumentIngestData(
  276. document=KnowledgeDocumentDto.from_entity(result.document),
  277. chunks=[KnowledgeChunkDto.from_entity(item) for item in result.chunks],
  278. queued=result.queued,
  279. job=result.job))
  280. @router.post("/jobs/reindex-base", response_model=ApiResponse[KnowledgeBaseReindexData])
  281. def reindex_base_contract(
  282. payload: KnowledgeBaseReindexRequestDto,
  283. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeBaseReindexData]:
  284. if service.base_repository.get_by_id(knowledge_base_id=payload.knowledgeBaseId) is None:
  285. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_base.not_found", id=payload.knowledgeBaseId))
  286. jobs = service.reindex_base_from_contract(payload)
  287. return ok(KnowledgeBaseReindexData(
  288. knowledgeBaseId=payload.knowledgeBaseId,
  289. queuedCount=len(jobs),
  290. jobs=jobs))
  291. @router.post("/documents/content", response_model=ApiResponse[KnowledgeDocumentContentData])
  292. def read_document_content_contract(
  293. payload: KnowledgeDocumentContentRequestDto,
  294. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentContentData]:
  295. try:
  296. result = service.read_document_content(
  297. document_id=payload.documentId,
  298. include_text=payload.includeText,
  299. include_base64=payload.includeBase64)
  300. except ObjectStorageError as exc:
  301. raise HTTPException(status_code=503, detail=error_detail("error.storage.unavailable", message=str(exc))) from exc
  302. except ValueError as exc:
  303. raise HTTPException(status_code=422, detail=error_detail("error.validation", message=str(exc))) from exc
  304. if result is None:
  305. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_document.not_found", id=payload.documentId))
  306. return ok(KnowledgeDocumentContentData.model_validate(result))
  307. @router.post("/documents/storage/status", response_model=ApiResponse[KnowledgeDocumentStorageStatusData])
  308. def read_document_storage_status_contract(
  309. payload: KnowledgeDocumentStorageStatusRequestDto,
  310. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentStorageStatusData]:
  311. try:
  312. result = service.read_document_storage_status(document_id=payload.documentId)
  313. except ObjectStorageError as exc:
  314. raise HTTPException(status_code=503, detail=error_detail("error.storage.unavailable", message=str(exc))) from exc
  315. if result is None:
  316. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_document.not_found", id=payload.documentId))
  317. return ok(KnowledgeDocumentStorageStatusData.model_validate({
  318. **result,
  319. "checkedTime": datetime.utcnow(),
  320. }))
  321. @router.post("/documents/parse", response_model=ApiResponse[KnowledgeDocumentParseData])
  322. def parse_document_contract(
  323. payload: KnowledgeDocumentParseRequestDto,
  324. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeDocumentParseData]:
  325. try:
  326. parsed = service.parse_document(
  327. KnowledgeDocumentParseRequest(
  328. source_type=payload.sourceType,
  329. source_uri=payload.sourceUri,
  330. content_text=payload.contentText,
  331. content_base64=payload.contentBase64))
  332. except DocumentParseError as exc:
  333. raise HTTPException(status_code=422, detail=error_detail("error.validation", message=str(exc))) from exc
  334. return ok(KnowledgeDocumentParseData(
  335. contentText=parsed.content_text,
  336. sourceType=parsed.source_type,
  337. metadata=parsed.metadata_json))
  338. @router.post("/chunks/list", response_model=ApiResponse[PageResult[KnowledgeChunkDto]])
  339. def list_chunks_contract(
  340. payload: KnowledgeChunkListRequestDto,
  341. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[PageResult[KnowledgeChunkDto]]:
  342. items = [
  343. KnowledgeChunkDto.from_entity(item)
  344. for item in service.list_chunks_filtered(
  345. knowledge_base_id=payload.knowledgeBaseId,
  346. document_id=payload.documentId,
  347. keyword=payload.keyword)
  348. ]
  349. return ok(paginate(items, page=payload.page, page_size=payload.pageSize))
  350. @router.post("/chunks/detail", response_model=ApiResponse[KnowledgeChunkDto])
  351. def detail_chunk_contract(
  352. payload: KnowledgeChunkDetailRequestDto,
  353. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeChunkDto]:
  354. entity = service.chunk_repository.get_by_id(chunk_id=payload.chunkId)
  355. if entity is None:
  356. raise HTTPException(status_code=404, detail=error_detail("error.knowledge_chunk.not_found", id=payload.chunkId))
  357. return ok(KnowledgeChunkDto.from_entity(entity))
  358. @router.post("/chunks/delete", response_model=ApiResponse[DeleteData])
  359. def delete_chunk_contract(
  360. payload: KnowledgeChunkDeleteRequestDto,
  361. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[DeleteData]:
  362. return ok(DeleteData(
  363. deleted=service.delete_chunk(chunk_id=payload.chunkId),
  364. chunkId=payload.chunkId))
  365. @router.post("/search/query", response_model=ApiResponse[list[KnowledgeSearchResultDto]])
  366. def search_contract(
  367. payload: KnowledgeSearchRequestDto,
  368. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[list[KnowledgeSearchResultDto]]:
  369. results = [
  370. KnowledgeSearchResultDto(
  371. chunk=KnowledgeChunkDto.from_entity(chunk),
  372. document=KnowledgeDocumentDto.from_entity(document),
  373. score=score,
  374. scoreDetails=score_json)
  375. for chunk, document, score, score_json in service.search(
  376. KnowledgeSearchRequest(
  377. knowledge_base_id=payload.knowledgeBaseId,
  378. query=payload.query,
  379. top_k=payload.topK,
  380. filters_json=payload.filters))
  381. ]
  382. return ok(results)
  383. @router.post("/settings/detail", response_model=ApiResponse[KnowledgeSettingsDto])
  384. def detail_settings_contract(
  385. payload: KnowledgeBaseDetailRequestDto,
  386. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeSettingsDto]:
  387. return ok(service.read_settings(knowledge_base_id=payload.knowledgeBaseId))
  388. @router.post("/settings/update", response_model=ApiResponse[KnowledgeSettingsDto])
  389. def update_settings_contract(
  390. payload: KnowledgeSettingsUpdateRequestDto,
  391. service: KnowledgeApplicationService = Depends(get_knowledge_application_service)) -> ApiResponse[KnowledgeSettingsDto]:
  392. return ok(service.update_settings(payload))