feat: add measured detection review loop
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-15 03:00:08 +02:00
parent 94ecd377b7
commit d22abe8e7b
27 changed files with 1578 additions and 29 deletions
+116 -17
View File
@@ -5,10 +5,11 @@ from uuid import UUID
from geoalchemy2.shape import to_shape
from shapely.geometry import mapping
from sqlalchemy import or_
from sqlalchemy.orm import Session
from app.core.errors import AppError
from app.models import Detection, QualityCheck, Segmentation, VectorFeature
from app.models import Detection, DetectionReview, QualityCheck, Segmentation, VectorFeature
class QualityEvidenceService:
@@ -21,8 +22,9 @@ class QualityEvidenceService:
findings = quality_check.findings_json or {}
features: list[dict[str, Any]] = []
warnings: list[str] = []
candidate_index = QualityEvidenceService._candidate_feature_index(db, quality_check)
reference_index = QualityEvidenceService._reference_feature_index(db, quality_check)
candidate_ids, reference_ids = QualityEvidenceService._evidence_identifiers(findings)
candidate_index = QualityEvidenceService._candidate_feature_index(db, quality_check, candidate_ids)
reference_index = QualityEvidenceService._reference_feature_index(db, quality_check, reference_ids)
for evidence in QualityEvidenceService._evidence_items(findings.get("match_evidence")):
candidate_id = QualityEvidenceService._string_value(evidence.get("candidate_feature_id"))
@@ -89,6 +91,8 @@ class QualityEvidenceService:
else:
warnings.append(f"False-negative evidence feature not found: {reference_id}")
QualityEvidenceService._annotate_reviews(db, quality_check, features)
return {
"quality_check_id": str(quality_check.id),
"project_id": str(quality_check.project_id),
@@ -117,34 +121,129 @@ class QualityEvidenceService:
return text or None
@staticmethod
def _candidate_feature_index(db: Session, quality_check: QualityCheck) -> dict[str, Any]:
def _evidence_identifiers(findings: dict[str, Any]) -> tuple[set[str], set[str]]:
candidate_ids: set[str] = set()
reference_ids: set[str] = set()
for evidence in QualityEvidenceService._evidence_items(findings.get("match_evidence")):
candidate_id = QualityEvidenceService._string_value(evidence.get("candidate_feature_id"))
reference_id = QualityEvidenceService._string_value(evidence.get("reference_feature_id"))
if candidate_id:
candidate_ids.add(candidate_id)
if reference_id:
reference_ids.add(reference_id)
for evidence in QualityEvidenceService._evidence_items(findings.get("false_positive_evidence")):
candidate_id = QualityEvidenceService._string_value(evidence.get("candidate_feature_id"))
if candidate_id:
candidate_ids.add(candidate_id)
for evidence in QualityEvidenceService._evidence_items(findings.get("false_negative_evidence")):
reference_id = QualityEvidenceService._string_value(evidence.get("reference_feature_id"))
if reference_id:
reference_ids.add(reference_id)
return candidate_ids, reference_ids
@staticmethod
def _uuid_identifiers(identifiers: set[str]) -> list[UUID]:
values: list[UUID] = []
for identifier in identifiers:
try:
values.append(UUID(identifier))
except (TypeError, ValueError):
continue
return values
@staticmethod
def _vector_feature_rows(db: Session, dataset_id: UUID, identifiers: set[str]) -> list[VectorFeature]:
if not identifiers:
return []
conditions = [VectorFeature.source_feature_id.in_(identifiers)]
uuid_identifiers = QualityEvidenceService._uuid_identifiers(identifiers)
if uuid_identifiers:
conditions.append(VectorFeature.id.in_(uuid_identifiers))
return (
db.query(VectorFeature)
.filter(VectorFeature.dataset_id == dataset_id, or_(*conditions))
.all()
)
@staticmethod
def _candidate_feature_index(
db: Session,
quality_check: QualityCheck,
identifiers: set[str],
) -> dict[str, Any]:
index: dict[str, Any] = {}
if not identifiers:
return index
uuid_identifiers = QualityEvidenceService._uuid_identifiers(identifiers)
if quality_check.candidate_dataset_id:
for row in db.query(VectorFeature).filter(VectorFeature.dataset_id == quality_check.candidate_dataset_id).all():
for row in QualityEvidenceService._vector_feature_rows(db, quality_check.candidate_dataset_id, identifiers):
QualityEvidenceService._add_index_keys(index, row)
for row in db.query(Detection).filter(Detection.dataset_id == quality_check.candidate_dataset_id).all():
QualityEvidenceService._add_index_keys(index, row)
for row in db.query(Segmentation).filter(Segmentation.dataset_id == quality_check.candidate_dataset_id).all():
QualityEvidenceService._add_index_keys(index, row)
if quality_check.analysis_run_id:
for row in db.query(Detection).filter(Detection.analysis_run_id == quality_check.analysis_run_id).all():
if uuid_identifiers:
for row in db.query(Detection).filter(
Detection.dataset_id == quality_check.candidate_dataset_id,
Detection.id.in_(uuid_identifiers),
).all():
QualityEvidenceService._add_index_keys(index, row)
for row in db.query(Segmentation).filter(Segmentation.analysis_run_id == quality_check.analysis_run_id).all():
for row in db.query(Segmentation).filter(
Segmentation.dataset_id == quality_check.candidate_dataset_id,
Segmentation.id.in_(uuid_identifiers),
).all():
QualityEvidenceService._add_index_keys(index, row)
elif quality_check.analysis_run_id:
for row in db.query(Detection).filter(Detection.analysis_run_id == quality_check.analysis_run_id).all():
if quality_check.analysis_run_id and uuid_identifiers:
for row in db.query(Detection).filter(
Detection.analysis_run_id == quality_check.analysis_run_id,
Detection.id.in_(uuid_identifiers),
).all():
QualityEvidenceService._add_index_keys(index, row)
for row in db.query(Segmentation).filter(Segmentation.analysis_run_id == quality_check.analysis_run_id).all():
for row in db.query(Segmentation).filter(
Segmentation.analysis_run_id == quality_check.analysis_run_id,
Segmentation.id.in_(uuid_identifiers),
).all():
QualityEvidenceService._add_index_keys(index, row)
return index
@staticmethod
def _reference_feature_index(db: Session, quality_check: QualityCheck) -> dict[str, Any]:
def _reference_feature_index(
db: Session,
quality_check: QualityCheck,
identifiers: set[str],
) -> dict[str, Any]:
index: dict[str, Any] = {}
for row in db.query(VectorFeature).filter(VectorFeature.dataset_id == quality_check.reference_dataset_id).all():
for row in QualityEvidenceService._vector_feature_rows(db, quality_check.reference_dataset_id, identifiers):
QualityEvidenceService._add_index_keys(index, row)
return index
@staticmethod
def _annotate_reviews(
db: Session,
quality_check: QualityCheck,
features: list[dict[str, Any]],
) -> None:
if quality_check.check_type != "detections_vs_reference":
return
reviews = db.query(DetectionReview).filter(DetectionReview.quality_check_id == quality_check.id).all()
review_index = {(row.evidence_role, row.evidence_feature_id): row for row in reviews}
for feature in features:
properties = feature.get("properties")
if not isinstance(properties, dict):
continue
role = QualityEvidenceService._string_value(properties.get("qa_evidence_role"))
if role == "false_positive":
evidence_id = QualityEvidenceService._string_value(properties.get("candidate_feature_id"))
elif role == "false_negative":
evidence_id = QualityEvidenceService._string_value(properties.get("reference_feature_id"))
else:
continue
review = review_index.get((role, evidence_id or ""))
properties.update(
{
"review_decision": review.decision if review else "unreviewed",
"review_notes": review.notes if review else None,
"reviewed_by": review.reviewed_by if review else None,
"reviewed_at": review.updated_at.isoformat() if review and review.updated_at else None,
}
)
@staticmethod
def _add_index_keys(index: dict[str, Any], row: Any) -> None:
for key in QualityEvidenceService._row_identifiers(row):