Polish grounded assistant output
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-16 03:54:25 +02:00
parent 1e23d30eaf
commit d0210b9a3c
4 changed files with 56 additions and 0 deletions
+3
View File
@@ -23,6 +23,9 @@
- Provisioned the five official thematic products and the 1,159-feature DOV
soil map into Mol's Area in the central 28-municipality workbench, removing
the mismatch with the earlier standalone Mol project.
- 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.
## Sprint 213-214 Cross-domain area profile (2026-07-16)
@@ -132,6 +132,26 @@ class GeoAssistantService:
f"{answer}"
)
@staticmethod
def rounded_context_value(value: float, unit: str) -> int | float:
normalized_unit = unit.casefold().strip()
if normalized_unit in {"inwoners", "personen", "objecten", "features"}:
return int(round(value))
if "%" in normalized_unit or "ha" in normalized_unit or "km" in normalized_unit or normalized_unit == "m":
return round(value, 2)
if "score" in normalized_unit:
return round(value, 4)
return round(value, 2)
@staticmethod
def normalize_plain_text(answer: str) -> str:
lines: list[str] = []
for line in answer.splitlines():
normalized = re.sub(r"^\s*\*\s+", "- ", line.strip())
normalized = normalized.replace("**", "").replace("__", "").replace("`", "")
lines.append(normalized)
return "\n".join(lines).strip()
def __init__(self, settings: Settings | None = None):
self.settings = settings or get_settings()
@@ -386,6 +406,7 @@ class GeoAssistantService:
)
context_metrics.append(item)
serialized_metrics.append(item.model_dump(mode="json"))
serialized_metrics[-1]["value"] = self.rounded_context_value(item.value, item.unit)
serialized_metrics[-1]["measurement_quality"] = (
"schatting" if item.is_estimate else "exact_binnen_bronrepresentatie"
)
@@ -428,6 +449,7 @@ class GeoAssistantService:
)
context_metrics.append(item)
serialized_metrics.append(item.model_dump(mode="json"))
serialized_metrics[-1]["value"] = self.rounded_context_value(item.value, item.unit)
serialized_metrics[-1]["measurement_quality"] = "resolutiegebonden_bronmeting"
source_dataset_ids.append(dataset.id)
current_context.append(
@@ -473,6 +495,7 @@ class GeoAssistantService:
)
context_metrics.append(item)
serialized_metrics.append(item.model_dump(mode="json"))
serialized_metrics[-1]["value"] = self.rounded_context_value(item.value, item.unit)
serialized_metrics[-1]["measurement_quality"] = "exacte_berekening_binnen_gemodelleerd_scenario"
source_dataset_ids.append(dataset.id)
current_context.append(
@@ -584,6 +607,8 @@ class GeoAssistantService:
"scope.label is het exact geanalyseerde gebied; vervang dit nooit door project.name of project.region. "
"Noem bij cijfers de bron en eenheid. Maak duidelijk onderscheid tussen exacte metingen en schattingen. "
"Als is_estimate true is, noem de waarde verplicht een schatting en nooit exact. "
"Een officiële bron maakt een afgeleide gebiedswaarde niet exact; noem een schatting nooit officieel geteld. "
"De numerieke contextwaarden zijn al bronveilig afgerond; neem die afgeronde waarden letterlijk over. "
"Objectaantallen zijn ondersteunend; geef betekenisvolle oppervlakte-, lengte- of bevolkingsmetriek voorrang. "
"Wanneer de gebruiker meerdere thema's opsomt, behandel elk gevraagd thema en voeg geen ongevraagd thema toe. "
"Houd het antwoord beknopt: groepeer de kernmetrieken per gevraagd thema en herhaal geen beperkingen. "
@@ -627,6 +652,7 @@ class GeoAssistantService:
answer = str(message.get("content") or "").strip()
if not answer:
raise AppError(code="OLLAMA_EMPTY_RESPONSE", message="Ollama gaf geen antwoord terug.", status_code=502)
answer = self.normalize_plain_text(answer)
answer = self.ensure_estimate_disclosure(answer, metrics)
return AssistantQueryResponse(
answer=answer,
@@ -165,6 +165,25 @@ def test_geo_assistant_does_not_add_irrelevant_estimate_disclosure() -> None:
assert answer == "De bosoppervlakte bedraagt 3.626,56 hectare."
@pytest.mark.parametrize(
("value", "unit", "expected"),
[
(36_782.6497, "inwoners", 36_783),
(3_638.4167, "ha", 3_638.42),
(31.76431, "%", 31.76),
(0.680612, "score", 0.6806),
],
)
def test_geo_assistant_rounds_prompt_values_by_semantic_unit(value: float, unit: str, expected: int | float) -> None:
assert GeoAssistantService.rounded_context_value(value, unit) == expected
def test_geo_assistant_normalizes_model_markdown_for_plain_text_renderer() -> None:
answer = GeoAssistantService.normalize_plain_text("**Bevolking**\n* 36.783 inwoners\n`Bron: Statbel`")
assert answer == "Bevolking\n- 36.783 inwoners\nBron: Statbel"
def test_geo_assistant_sends_grounded_context_without_thinking_trace(monkeypatch) -> None:
service = GeoAssistantService(ollama_settings())
project_id = uuid4()
@@ -228,6 +247,8 @@ def test_geo_assistant_sends_grounded_context_without_thinking_trace(monkeypatch
assert "Gebruik uitsluitend feiten en cijfers uit CONTEXT_JSON" in captured["payload"]["messages"][0]["content"]
assert "scope.label is het exact geanalyseerde gebied" in captured["payload"]["messages"][0]["content"]
assert "noem de waarde verplicht een schatting" in captured["payload"]["messages"][0]["content"]
assert "noem een schatting nooit officieel geteld" in captured["payload"]["messages"][0]["content"]
assert "contextwaarden zijn al bronveilig afgerond" in captured["payload"]["messages"][0]["content"]
assert "verzin geen oorzaak, voorspelling, verzadiging" in captured["payload"]["messages"][0]["content"]
assert "bereken zelf geen gemiddelde, tempo, oorzaak of afgeleide trend" in captured["payload"]["messages"][0]["content"]
assert "zonder Markdown-symbolen" in captured["payload"]["messages"][0]["content"]
+6
View File
@@ -16,6 +16,10 @@ Changed:
- Added deterministic Dutch theme selection before GIS calculation. Explicit
questions now calculate only named themes; general summaries and source
inventory questions deliberately retain full context.
- The optimized live response completed in 12.3 seconds but exposed literal
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.
Validation evidence:
- A read-only live production-chain probe with the 1,200-token setting
@@ -27,6 +31,8 @@ Validation evidence:
- After deterministic theme filtering was added, the complete readiness gate
passed 720 backend tests plus all compile, contract, Alembic, frontend and
shell gates.
- After plain-text normalization and semantic prompt rounding, the final
readiness rerun passed 725 backend tests and the same complete gate set.
- 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