diff --git a/CHANGELOG.md b/CHANGELOG.md index 07ab849b..2833ba66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ - Added deterministic semantic rounding for model context and normalized model Markdown to the existing plain-text renderer so hectare, population and score answers remain readable without exposing raw floating-point tails. +- Prevented an area-weighted population estimate from retaining model phrases + such as "official count" when its persisted metric is marked as estimated. ## Sprint 213-214 Cross-domain area profile (2026-07-16) diff --git a/backend/app/services/geo_assistant_service.py b/backend/app/services/geo_assistant_service.py index 85f843c8..2fd867f5 100644 --- a/backend/app/services/geo_assistant_service.py +++ b/backend/app/services/geo_assistant_service.py @@ -108,13 +108,27 @@ class GeoAssistantService: answer: str, metrics: list[AssistantContextMetric], ) -> str: + estimated_themes = {metric.theme for metric in metrics if metric.is_estimate} + if "population" in estimated_themes: + answer = re.sub( + r"\bde officiële telling\b", + "de van de officiële bron afgeleide schatting", + answer, + flags=re.IGNORECASE, + ) + answer = re.sub( + r"\bofficieel geteld aantal inwoners\b", + "uit een officiële bron afgeleid aantal inwoners", + answer, + flags=re.IGNORECASE, + ) normalized = answer.casefold() if "schat" in normalized: return answer disclosed_themes = { metric.theme for metric in metrics - if metric.is_estimate + if metric.theme in estimated_themes and any( term in normalized for term in cls.ESTIMATE_TOPIC_TERMS.get(metric.theme, (metric.label.casefold(),)) diff --git a/backend/tests/test_sprint202_temporal_metrics_and_ollama.py b/backend/tests/test_sprint202_temporal_metrics_and_ollama.py index 17231571..8c604bb5 100644 --- a/backend/tests/test_sprint202_temporal_metrics_and_ollama.py +++ b/backend/tests/test_sprint202_temporal_metrics_and_ollama.py @@ -144,6 +144,28 @@ def test_geo_assistant_discloses_estimated_population_values() -> None: ) +def test_geo_assistant_never_labels_area_weighted_population_as_official_count() -> None: + metrics = [ + AssistantContextMetric( + theme="population", + label="Geraamd aantal inwoners", + value=38_675, + unit="inwoners", + source="Statbel", + dataset_id=uuid4(), + is_estimate=True, + ) + ] + + answer = GeoAssistantService.ensure_estimate_disclosure( + "De officiële telling uit januari 2025 bedraagt 38.675 inwoners. Deze waarde is een schatting.", + metrics, + ) + + assert "officiële telling" not in answer.casefold() + assert answer.startswith("de van de officiële bron afgeleide schatting") + + def test_geo_assistant_does_not_add_irrelevant_estimate_disclosure() -> None: metrics = [ AssistantContextMetric( diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index b303a8ef..077919b7 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -20,6 +20,10 @@ Changed: Markdown and raw floating-point tails in the plain-text chat renderer. Added deterministic Markdown normalization and unit-aware context rounding while retaining the unrounded metrics in the canonical API response. +- The next live answer still called an area-weighted Statbel population value + an official count before acknowledging it as an estimate. The deterministic + estimate guard now removes that contradictory label whenever persisted + population context is estimated. Validation evidence: - A read-only live production-chain probe with the 1,200-token setting @@ -33,6 +37,8 @@ Validation evidence: shell gates. - After plain-text normalization and semantic prompt rounding, the final readiness rerun passed 725 backend tests and the same complete gate set. +- After hardening estimated-population wording, the final complete readiness + rerun passed 726 backend tests and all remaining gates unchanged. - The regional thematic operator dry-run resolved exactly 28 official municipality Areas. The live Mol run in `Kempen Regional Workbench` imported all five products with complete source coverage; the DOV operator imported