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 - 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 soil map into Mol's Area in the central 28-municipality workbench, removing
the mismatch with the earlier standalone Mol project. 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) ## Sprint 213-214 Cross-domain area profile (2026-07-16)
@@ -132,6 +132,26 @@ class GeoAssistantService:
f"{answer}" 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): def __init__(self, settings: Settings | None = None):
self.settings = settings or get_settings() self.settings = settings or get_settings()
@@ -386,6 +406,7 @@ class GeoAssistantService:
) )
context_metrics.append(item) context_metrics.append(item)
serialized_metrics.append(item.model_dump(mode="json")) 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"] = ( serialized_metrics[-1]["measurement_quality"] = (
"schatting" if item.is_estimate else "exact_binnen_bronrepresentatie" "schatting" if item.is_estimate else "exact_binnen_bronrepresentatie"
) )
@@ -428,6 +449,7 @@ class GeoAssistantService:
) )
context_metrics.append(item) context_metrics.append(item)
serialized_metrics.append(item.model_dump(mode="json")) 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" serialized_metrics[-1]["measurement_quality"] = "resolutiegebonden_bronmeting"
source_dataset_ids.append(dataset.id) source_dataset_ids.append(dataset.id)
current_context.append( current_context.append(
@@ -473,6 +495,7 @@ class GeoAssistantService:
) )
context_metrics.append(item) context_metrics.append(item)
serialized_metrics.append(item.model_dump(mode="json")) 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" serialized_metrics[-1]["measurement_quality"] = "exacte_berekening_binnen_gemodelleerd_scenario"
source_dataset_ids.append(dataset.id) source_dataset_ids.append(dataset.id)
current_context.append( 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. " "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. " "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. " "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. " "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. " "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. " "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() answer = str(message.get("content") or "").strip()
if not answer: if not answer:
raise AppError(code="OLLAMA_EMPTY_RESPONSE", message="Ollama gaf geen antwoord terug.", status_code=502) 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) answer = self.ensure_estimate_disclosure(answer, metrics)
return AssistantQueryResponse( return AssistantQueryResponse(
answer=answer, 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." 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: def test_geo_assistant_sends_grounded_context_without_thinking_trace(monkeypatch) -> None:
service = GeoAssistantService(ollama_settings()) service = GeoAssistantService(ollama_settings())
project_id = uuid4() 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 "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 "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 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 "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 "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"] 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 - Added deterministic Dutch theme selection before GIS calculation. Explicit
questions now calculate only named themes; general summaries and source questions now calculate only named themes; general summaries and source
inventory questions deliberately retain full context. 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: Validation evidence:
- A read-only live production-chain probe with the 1,200-token setting - 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 - After deterministic theme filtering was added, the complete readiness gate
passed 720 backend tests plus all compile, contract, Alembic, frontend and passed 720 backend tests plus all compile, contract, Alembic, frontend and
shell gates. 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 - The regional thematic operator dry-run resolved exactly 28 official
municipality Areas. The live Mol run in `Kempen Regional Workbench` imported municipality Areas. The live Mol run in `Kempen Regional Workbench` imported
all five products with complete source coverage; the DOV operator imported all five products with complete source coverage; the DOV operator imported