Polish grounded assistant output
This commit is contained in:
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user