distinguish a redrawn footprint from a demolition, and derive estimate
disclosure from data Change detection had only added/removed/unchanged, so a building extended by an annexe dropped below the IoU threshold and was reported twice: once as removed and once as added. That hides exactly the category a change-detection product exists to show and inflates both counts. A "modified" class now covers the band between the modified floor and the unchanged threshold. Matching also ran as a full cross product with no spatial index, unlike the QA matcher beside it: two municipal building layers meant hundreds of millions of geometry intersections. It uses an STRtree and considers larger footprints first, so a big footprint is not left over after a small neighbour claimed its counterpart. The assistant guaranteed honesty about estimated values by rewriting the model's sentences with regular expressions, which only fires when it recognises the phrasing the model happened to produce. estimate_disclosures derives the same statement from the metric metadata, so it holds regardless of how the answer was worded. The prose substitution stays as a second layer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ from app.core.errors import AppError
|
||||
from app.models import Area, Dataset, Project
|
||||
from app.schemas.assistant import (
|
||||
AssistantContextMetric,
|
||||
AssistantEstimateDisclosure,
|
||||
AssistantModelRead,
|
||||
AssistantQueryRequest,
|
||||
AssistantQueryResponse,
|
||||
@@ -112,6 +113,44 @@ class GeoAssistantService:
|
||||
}
|
||||
return themes or None
|
||||
|
||||
@classmethod
|
||||
def estimate_disclosures(
|
||||
cls,
|
||||
metrics: list[AssistantContextMetric],
|
||||
) -> list[AssistantEstimateDisclosure]:
|
||||
"""List every estimated value behind the answer, straight from metadata.
|
||||
|
||||
``ensure_estimate_disclosure`` can only add a caveat when it recognises
|
||||
the phrasing the model produced, which makes the guarantee dependent on
|
||||
generated text. This derives the same statement from the source
|
||||
metadata, so it holds regardless of how the answer was written.
|
||||
"""
|
||||
|
||||
seen: set[tuple[str, UUID]] = set()
|
||||
disclosures: list[AssistantEstimateDisclosure] = []
|
||||
for metric in sorted(metrics, key=lambda item: (item.theme, item.label)):
|
||||
if not metric.is_estimate:
|
||||
continue
|
||||
key = (metric.theme, metric.dataset_id)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
topic = cls.ESTIMATE_TOPIC_LABELS.get(metric.theme, metric.label)
|
||||
disclosures.append(
|
||||
AssistantEstimateDisclosure(
|
||||
theme=metric.theme,
|
||||
label=metric.label,
|
||||
unit=metric.unit,
|
||||
source=metric.source,
|
||||
dataset_id=metric.dataset_id,
|
||||
reason=(
|
||||
f"De bronmetadata van {metric.source} markeert {topic} als schatting, "
|
||||
"geen exacte telling."
|
||||
),
|
||||
)
|
||||
)
|
||||
return disclosures
|
||||
|
||||
@classmethod
|
||||
def ensure_estimate_disclosure(
|
||||
cls,
|
||||
@@ -702,6 +741,7 @@ class GeoAssistantService:
|
||||
scope_label=scope_label,
|
||||
context_metrics=metrics,
|
||||
temporal_series=series,
|
||||
estimate_disclosures=self.estimate_disclosures(metrics),
|
||||
source_dataset_ids=dataset_ids,
|
||||
warnings=warnings,
|
||||
generated_at=datetime.now(timezone.utc),
|
||||
|
||||
Reference in New Issue
Block a user