| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- from __future__ import annotations
- import shutil
- from dataclasses import dataclass
- from datetime import datetime, timezone
- from pathlib import Path
- from sqlalchemy.orm import Session
- from app.core.config import settings
- from app.core.errors import APIError
- from app.db.models.document import BusinessDocument, DocumentExtraction
- from app.db.repositories import document as document_repo
- from app.db.session import SessionLocal
- from app.schemas.document import (
- BusinessDocumentCreate,
- BusinessDocumentUpdate,
- DocumentExtractionCreate,
- DocumentExtractionUpdate,
- )
- from app.services.extraction_accuracy import judge_extraction_edit
- @dataclass
- class ExtractionEditTask:
- extraction_id: str
- edit_count: int
- field_name: str
- original_value: str
- edited_value: str
- evidence_text: str
- document_title: str
- def create_document(
- db: Session,
- payload: BusinessDocumentCreate,
- ) -> BusinessDocument:
- data = payload.model_dump(exclude={"metadata"})
- data["metadata_json"] = payload.metadata
- return document_repo.create_document(db, data)
- def update_document(
- db: Session,
- document_id: str,
- payload: BusinessDocumentUpdate,
- ) -> BusinessDocument:
- document = document_repo.get_document(db, document_id)
- if document is None:
- raise APIError(404, "document_not_found", "业务资料不存在")
- updates = payload.model_dump(exclude_unset=True)
- if "metadata" in updates:
- updates["metadata_json"] = updates.pop("metadata")
- return document_repo.update_document(db, document, updates)
- def get_document_detail(
- db: Session,
- document_id: str,
- ) -> tuple[BusinessDocument, list[DocumentExtraction]]:
- document = document_repo.get_document(db, document_id)
- if document is None:
- raise APIError(404, "document_not_found", "业务资料不存在")
- return document, document_repo.list_extractions(db, document_id)
- def add_extraction(
- db: Session,
- payload: DocumentExtractionCreate,
- ) -> DocumentExtraction:
- if document_repo.get_document(db, payload.document_id) is None:
- raise APIError(404, "document_not_found", "业务资料不存在")
- data = payload.model_dump(exclude={"metadata"})
- data["metadata_json"] = payload.metadata
- return document_repo.create_extraction(db, data)
- def update_extraction(
- db: Session,
- document_id: str,
- extraction_id: str,
- payload: DocumentExtractionUpdate,
- ) -> tuple[DocumentExtraction, ExtractionEditTask | None]:
- extraction = document_repo.get_extraction(db, extraction_id)
- if extraction is None or extraction.document_id != document_id:
- raise APIError(404, "document_extraction_not_found", "结构化字段不存在")
- updates = payload.model_dump(exclude_unset=True)
- new_value = updates.get("normalized_value")
- if new_value is None:
- new_value = updates.get("field_value")
- previous_value = extraction.normalized_value or extraction.field_value or ""
- task: ExtractionEditTask | None = None
- if new_value is not None and str(new_value) != str(previous_value):
- document = document_repo.get_document(db, document_id)
- if not extraction.original_value:
- updates["original_value"] = previous_value
- updates["edited"] = True
- updates["edit_count"] = int(extraction.edit_count or 0) + 1
- reverted_to_original = bool(extraction.edited) and (
- str(new_value).strip() == str(extraction.original_value or "").strip()
- )
- if reverted_to_original:
- updates["edited"] = False
- updates["extraction_correct"] = True
- updates["edit_judgement_status"] = "completed"
- updates["edit_reason"] = "用户最终恢复为原始抽取值,字段按正确计算。"
- updates["edit_judgement_source"] = "reverted"
- updates["edit_judgement_confidence"] = 1.0
- updates["edit_judged_at"] = datetime.now(timezone.utc)
- else:
- updates["edit_judgement_status"] = "pending"
- updates["edit_reason"] = ""
- updates["edit_judgement_source"] = ""
- updates["edit_judgement_confidence"] = None
- updates["edit_judged_at"] = None
- task = ExtractionEditTask(
- extraction_id=extraction.id,
- edit_count=int(updates["edit_count"]),
- field_name=extraction.field_name or extraction.field_key,
- original_value=previous_value,
- edited_value=str(new_value),
- evidence_text=extraction.evidence_text or "",
- document_title=document.title if document else "",
- )
- updates["verified"] = True
- updates["verified_by"] = payload.verified_by or "前端用户"
- saved = document_repo.update_extraction(db, extraction, updates)
- return saved, task
- async def process_extraction_edit_judgement(task: ExtractionEditTask) -> None:
- judgement = await judge_extraction_edit(
- field_name=task.field_name,
- original_value=task.original_value,
- edited_value=task.edited_value,
- evidence_text=task.evidence_text,
- document_title=task.document_title,
- extraction_id=task.extraction_id,
- )
- with SessionLocal() as db:
- extraction = document_repo.get_extraction(db, task.extraction_id)
- if extraction is None or int(extraction.edit_count or 0) != task.edit_count:
- return
- current_value = extraction.normalized_value or extraction.field_value or ""
- if str(current_value) != task.edited_value:
- return
- document_repo.update_extraction(
- db,
- extraction,
- {
- "extraction_correct": bool(extraction.extraction_correct)
- and not judgement.is_extraction_error,
- "edit_reason": judgement.reason,
- "edit_judgement_source": judgement.source,
- "edit_judgement_confidence": judgement.confidence,
- "edit_judgement_status": "completed",
- "edit_judged_at": datetime.now(timezone.utc),
- },
- )
- def delete_document(db: Session, document_id: str) -> None:
- document = document_repo.get_document(db, document_id)
- if document is None:
- raise APIError(404, "document_not_found", "业务资料不存在")
- upload_root = settings.upload_path.resolve()
- stored_file = Path(document.file_path)
- if stored_file.exists():
- resolved_file = stored_file.resolve()
- if resolved_file.is_relative_to(upload_root):
- resolved_file.unlink(missing_ok=True)
- parsed_dir = upload_root / "parsed" / document.id
- if parsed_dir.exists():
- resolved_parsed = parsed_dir.resolve()
- if resolved_parsed.is_relative_to(upload_root):
- shutil.rmtree(resolved_parsed)
- document_repo.delete_document(db, document)
|