document_service.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. from __future__ import annotations
  2. import shutil
  3. from dataclasses import dataclass
  4. from datetime import datetime, timezone
  5. from pathlib import Path
  6. from sqlalchemy.orm import Session
  7. from app.core.config import settings
  8. from app.core.errors import APIError
  9. from app.db.models.document import BusinessDocument, DocumentExtraction
  10. from app.db.repositories import document as document_repo
  11. from app.db.session import SessionLocal
  12. from app.schemas.document import (
  13. BusinessDocumentCreate,
  14. BusinessDocumentUpdate,
  15. DocumentExtractionCreate,
  16. DocumentExtractionUpdate,
  17. )
  18. from app.services.extraction_accuracy import judge_extraction_edit
  19. @dataclass
  20. class ExtractionEditTask:
  21. extraction_id: str
  22. edit_count: int
  23. field_name: str
  24. original_value: str
  25. edited_value: str
  26. evidence_text: str
  27. document_title: str
  28. def create_document(
  29. db: Session,
  30. payload: BusinessDocumentCreate,
  31. ) -> BusinessDocument:
  32. data = payload.model_dump(exclude={"metadata"})
  33. data["metadata_json"] = payload.metadata
  34. return document_repo.create_document(db, data)
  35. def update_document(
  36. db: Session,
  37. document_id: str,
  38. payload: BusinessDocumentUpdate,
  39. ) -> BusinessDocument:
  40. document = document_repo.get_document(db, document_id)
  41. if document is None:
  42. raise APIError(404, "document_not_found", "业务资料不存在")
  43. updates = payload.model_dump(exclude_unset=True)
  44. if "metadata" in updates:
  45. updates["metadata_json"] = updates.pop("metadata")
  46. return document_repo.update_document(db, document, updates)
  47. def get_document_detail(
  48. db: Session,
  49. document_id: str,
  50. ) -> tuple[BusinessDocument, list[DocumentExtraction]]:
  51. document = document_repo.get_document(db, document_id)
  52. if document is None:
  53. raise APIError(404, "document_not_found", "业务资料不存在")
  54. return document, document_repo.list_extractions(db, document_id)
  55. def add_extraction(
  56. db: Session,
  57. payload: DocumentExtractionCreate,
  58. ) -> DocumentExtraction:
  59. if document_repo.get_document(db, payload.document_id) is None:
  60. raise APIError(404, "document_not_found", "业务资料不存在")
  61. data = payload.model_dump(exclude={"metadata"})
  62. data["metadata_json"] = payload.metadata
  63. return document_repo.create_extraction(db, data)
  64. def update_extraction(
  65. db: Session,
  66. document_id: str,
  67. extraction_id: str,
  68. payload: DocumentExtractionUpdate,
  69. ) -> tuple[DocumentExtraction, ExtractionEditTask | None]:
  70. extraction = document_repo.get_extraction(db, extraction_id)
  71. if extraction is None or extraction.document_id != document_id:
  72. raise APIError(404, "document_extraction_not_found", "结构化字段不存在")
  73. updates = payload.model_dump(exclude_unset=True)
  74. new_value = updates.get("normalized_value")
  75. if new_value is None:
  76. new_value = updates.get("field_value")
  77. previous_value = extraction.normalized_value or extraction.field_value or ""
  78. task: ExtractionEditTask | None = None
  79. if new_value is not None and str(new_value) != str(previous_value):
  80. document = document_repo.get_document(db, document_id)
  81. if not extraction.original_value:
  82. updates["original_value"] = previous_value
  83. updates["edited"] = True
  84. updates["edit_count"] = int(extraction.edit_count or 0) + 1
  85. reverted_to_original = bool(extraction.edited) and (
  86. str(new_value).strip() == str(extraction.original_value or "").strip()
  87. )
  88. if reverted_to_original:
  89. updates["edited"] = False
  90. updates["extraction_correct"] = True
  91. updates["edit_judgement_status"] = "completed"
  92. updates["edit_reason"] = "用户最终恢复为原始抽取值,字段按正确计算。"
  93. updates["edit_judgement_source"] = "reverted"
  94. updates["edit_judgement_confidence"] = 1.0
  95. updates["edit_judged_at"] = datetime.now(timezone.utc)
  96. else:
  97. updates["edit_judgement_status"] = "pending"
  98. updates["edit_reason"] = ""
  99. updates["edit_judgement_source"] = ""
  100. updates["edit_judgement_confidence"] = None
  101. updates["edit_judged_at"] = None
  102. task = ExtractionEditTask(
  103. extraction_id=extraction.id,
  104. edit_count=int(updates["edit_count"]),
  105. field_name=extraction.field_name or extraction.field_key,
  106. original_value=previous_value,
  107. edited_value=str(new_value),
  108. evidence_text=extraction.evidence_text or "",
  109. document_title=document.title if document else "",
  110. )
  111. updates["verified"] = True
  112. updates["verified_by"] = payload.verified_by or "前端用户"
  113. saved = document_repo.update_extraction(db, extraction, updates)
  114. return saved, task
  115. async def process_extraction_edit_judgement(task: ExtractionEditTask) -> None:
  116. judgement = await judge_extraction_edit(
  117. field_name=task.field_name,
  118. original_value=task.original_value,
  119. edited_value=task.edited_value,
  120. evidence_text=task.evidence_text,
  121. document_title=task.document_title,
  122. extraction_id=task.extraction_id,
  123. )
  124. with SessionLocal() as db:
  125. extraction = document_repo.get_extraction(db, task.extraction_id)
  126. if extraction is None or int(extraction.edit_count or 0) != task.edit_count:
  127. return
  128. current_value = extraction.normalized_value or extraction.field_value or ""
  129. if str(current_value) != task.edited_value:
  130. return
  131. document_repo.update_extraction(
  132. db,
  133. extraction,
  134. {
  135. "extraction_correct": bool(extraction.extraction_correct)
  136. and not judgement.is_extraction_error,
  137. "edit_reason": judgement.reason,
  138. "edit_judgement_source": judgement.source,
  139. "edit_judgement_confidence": judgement.confidence,
  140. "edit_judgement_status": "completed",
  141. "edit_judged_at": datetime.now(timezone.utc),
  142. },
  143. )
  144. def delete_document(db: Session, document_id: str) -> None:
  145. document = document_repo.get_document(db, document_id)
  146. if document is None:
  147. raise APIError(404, "document_not_found", "业务资料不存在")
  148. upload_root = settings.upload_path.resolve()
  149. stored_file = Path(document.file_path)
  150. if stored_file.exists():
  151. resolved_file = stored_file.resolve()
  152. if resolved_file.is_relative_to(upload_root):
  153. resolved_file.unlink(missing_ok=True)
  154. parsed_dir = upload_root / "parsed" / document.id
  155. if parsed_dir.exists():
  156. resolved_parsed = parsed_dir.resolve()
  157. if resolved_parsed.is_relative_to(upload_root):
  158. shutil.rmtree(resolved_parsed)
  159. document_repo.delete_document(db, document)