fix: prevent truncated Ollama answers
This commit is contained in:
@@ -22,6 +22,8 @@
|
|||||||
refuses to infer unavailable values such as water volume.
|
refuses to infer unavailable values such as water volume.
|
||||||
- Added editable Unraid environment/template settings and a Docker host-gateway
|
- Added editable Unraid environment/template settings and a Docker host-gateway
|
||||||
mapping for the Ollama service running on the server.
|
mapping for the Ollama service running on the server.
|
||||||
|
- Added an explicit 16,384-token Ollama context window and reject truncated
|
||||||
|
`done_reason=length` responses instead of showing an incomplete answer.
|
||||||
|
|
||||||
## Sprint 201 Semantic area-selection metrics (2026-07-15)
|
## Sprint 201 Semantic area-selection metrics (2026-07-15)
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -1100,6 +1100,7 @@ OLLAMA_BASE_URL=http://host.docker.internal:11434
|
|||||||
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
|
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
|
||||||
OLLAMA_TIMEOUT_SECONDS=120
|
OLLAMA_TIMEOUT_SECONDS=120
|
||||||
OLLAMA_MAX_OUTPUT_TOKENS=700
|
OLLAMA_MAX_OUTPUT_TOKENS=700
|
||||||
|
OLLAMA_CONTEXT_TOKENS=16384
|
||||||
```
|
```
|
||||||
|
|
||||||
The Unraid deployment adds `host.docker.internal:host-gateway` automatically.
|
The Unraid deployment adds `host.docker.internal:host-gateway` automatically.
|
||||||
@@ -1107,7 +1108,9 @@ Verify the connection with `GET /api/v1/assistant/status`, inspect installed
|
|||||||
models with `GET /api/v1/assistant/models` and ask a grounded question through
|
models with `GET /api/v1/assistant/models` and ask a grounded question through
|
||||||
`POST /api/v1/projects/{project_id}/assistant/query`. A requested model must be
|
`POST /api/v1/projects/{project_id}/assistant/query`. A requested model must be
|
||||||
present in Ollama `/api/tags`. Missing water depth/bathymetry remains explicit;
|
present in Ollama `/api/tags`. Missing water depth/bathymetry remains explicit;
|
||||||
the assistant cannot turn 2D water geometry into volume.
|
the assistant cannot turn 2D water geometry into volume. GeoIntel rejects an
|
||||||
|
answer when Ollama reports `done_reason=length`, so a visibly truncated sentence
|
||||||
|
is never presented as a complete result.
|
||||||
|
|
||||||
## Helpful repository scripts
|
## Helpful repository scripts
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ class Settings(BaseSettings):
|
|||||||
ollama_default_model: str = Field(default="qwen3.5:9b", validation_alias="OLLAMA_DEFAULT_MODEL")
|
ollama_default_model: str = Field(default="qwen3.5:9b", validation_alias="OLLAMA_DEFAULT_MODEL")
|
||||||
ollama_timeout_seconds: int = Field(default=120, ge=5, le=600, validation_alias="OLLAMA_TIMEOUT_SECONDS")
|
ollama_timeout_seconds: int = Field(default=120, ge=5, le=600, validation_alias="OLLAMA_TIMEOUT_SECONDS")
|
||||||
ollama_max_output_tokens: int = Field(default=700, ge=100, le=4_000, validation_alias="OLLAMA_MAX_OUTPUT_TOKENS")
|
ollama_max_output_tokens: int = Field(default=700, ge=100, le=4_000, validation_alias="OLLAMA_MAX_OUTPUT_TOKENS")
|
||||||
|
ollama_context_tokens: int = Field(default=16_384, ge=4_096, le=131_072, validation_alias="OLLAMA_CONTEXT_TOKENS")
|
||||||
cors_origins: list[str] | str = Field(
|
cors_origins: list[str] | str = Field(
|
||||||
default=["http://localhost:5173", "http://127.0.0.1:5173"],
|
default=["http://localhost:5173", "http://127.0.0.1:5173"],
|
||||||
validation_alias="CORS_ORIGINS",
|
validation_alias="CORS_ORIGINS",
|
||||||
|
|||||||
@@ -367,9 +367,23 @@ class GeoAssistantService:
|
|||||||
"stream": False,
|
"stream": False,
|
||||||
"think": False,
|
"think": False,
|
||||||
"keep_alive": "10m",
|
"keep_alive": "10m",
|
||||||
"options": {"temperature": 0.1, "num_predict": self.settings.ollama_max_output_tokens},
|
"options": {
|
||||||
|
"temperature": 0.1,
|
||||||
|
"num_ctx": self.settings.ollama_context_tokens,
|
||||||
|
"num_predict": self.settings.ollama_max_output_tokens,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if response.get("done_reason") == "length":
|
||||||
|
raise AppError(
|
||||||
|
code="OLLAMA_RESPONSE_TRUNCATED",
|
||||||
|
message="Ollama kon geen volledig antwoord binnen de ingestelde contextlimiet genereren.",
|
||||||
|
details={
|
||||||
|
"context_tokens": self.settings.ollama_context_tokens,
|
||||||
|
"max_output_tokens": self.settings.ollama_max_output_tokens,
|
||||||
|
},
|
||||||
|
status_code=502,
|
||||||
|
)
|
||||||
message = response.get("message") if isinstance(response.get("message"), dict) else {}
|
message = response.get("message") if isinstance(response.get("message"), dict) else {}
|
||||||
answer = str(message.get("content") or "").strip()
|
answer = str(message.get("content") or "").strip()
|
||||||
if not answer:
|
if not answer:
|
||||||
|
|||||||
@@ -155,11 +155,57 @@ def test_geo_assistant_sends_grounded_context_without_thinking_trace(monkeypatch
|
|||||||
assert captured["path"] == "/api/chat"
|
assert captured["path"] == "/api/chat"
|
||||||
assert captured["payload"]["stream"] is False
|
assert captured["payload"]["stream"] is False
|
||||||
assert captured["payload"]["think"] is False
|
assert captured["payload"]["think"] is False
|
||||||
|
assert captured["payload"]["options"]["num_ctx"] == 16_384
|
||||||
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 "water_volume_available" in captured["payload"]["messages"][0]["content"]
|
assert "water_volume_available" in captured["payload"]["messages"][0]["content"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_geo_assistant_rejects_truncated_ollama_answer(monkeypatch) -> None:
|
||||||
|
service = GeoAssistantService(ollama_settings())
|
||||||
|
monkeypatch.setattr(service, "list_models", lambda: [AssistantModelRead(name="qwen3.5:9b")])
|
||||||
|
monkeypatch.setattr(
|
||||||
|
service,
|
||||||
|
"_build_context",
|
||||||
|
lambda *args, **kwargs: (
|
||||||
|
{"scope": {"label": "Gemeente Mol"}},
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
"Gemeente Mol",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
service,
|
||||||
|
"_request_json",
|
||||||
|
lambda path, payload=None: {
|
||||||
|
"done": True,
|
||||||
|
"done_reason": "length",
|
||||||
|
"message": {"role": "assistant", "content": "Een onvolledige zin"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(AppError) as exc_info:
|
||||||
|
service.query(
|
||||||
|
object(),
|
||||||
|
project_id=uuid4(),
|
||||||
|
payload=AssistantQueryRequest(question="Hoe evolueerde Mol?"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.code == "OLLAMA_RESPONSE_TRUNCATED"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unraid_ollama_context_window_is_configurable() -> None:
|
||||||
|
compose = (ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8")
|
||||||
|
template = (ROOT / "deploy/unraid/geointel-unraid-template.xml").read_text(encoding="utf-8")
|
||||||
|
env_example = (ROOT / "deploy/unraid/geointel.env.example").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "OLLAMA_CONTEXT_TOKENS: ${OLLAMA_CONTEXT_TOKENS:-16384}" in compose
|
||||||
|
assert 'Target="OLLAMA_CONTEXT_TOKENS"' in template
|
||||||
|
assert "OLLAMA_CONTEXT_TOKENS=16384" in env_example
|
||||||
|
|
||||||
|
|
||||||
def test_temporal_comparison_preserves_all_compatible_semantic_metrics() -> None:
|
def test_temporal_comparison_preserves_all_compatible_semantic_metrics() -> None:
|
||||||
earlier = {
|
earlier = {
|
||||||
"metrics": [
|
"metrics": [
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ OLLAMA_BASE_URL=http://host.docker.internal:11434
|
|||||||
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
|
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
|
||||||
OLLAMA_TIMEOUT_SECONDS=120
|
OLLAMA_TIMEOUT_SECONDS=120
|
||||||
OLLAMA_MAX_OUTPUT_TOKENS=700
|
OLLAMA_MAX_OUTPUT_TOKENS=700
|
||||||
|
OLLAMA_CONTEXT_TOKENS=16384
|
||||||
```
|
```
|
||||||
|
|
||||||
The model dropdown comes from Ollama `/api/tags`, so changing the installed
|
The model dropdown comes from Ollama `/api/tags`, so changing the installed
|
||||||
|
|||||||
@@ -38,4 +38,5 @@
|
|||||||
<Config Name="Ollama Base URL" Target="OLLAMA_BASE_URL" Default="http://host.docker.internal:11434" Mode="" Description="Ollama API reachable from the container. The deployment maps host.docker.internal to the Unraid host gateway." Type="Variable" Display="always" Required="true" Mask="false">http://host.docker.internal:11434</Config>
|
<Config Name="Ollama Base URL" Target="OLLAMA_BASE_URL" Default="http://host.docker.internal:11434" Mode="" Description="Ollama API reachable from the container. The deployment maps host.docker.internal to the Unraid host gateway." Type="Variable" Display="always" Required="true" Mask="false">http://host.docker.internal:11434</Config>
|
||||||
<Config Name="Default Ollama Model" Target="OLLAMA_DEFAULT_MODEL" Default="qwen3.5:9b" Mode="" Description="Preferred locally installed Ollama model. Users can select another installed model in GeoIntel." Type="Variable" Display="always" Required="true" Mask="false">qwen3.5:9b</Config>
|
<Config Name="Default Ollama Model" Target="OLLAMA_DEFAULT_MODEL" Default="qwen3.5:9b" Mode="" Description="Preferred locally installed Ollama model. Users can select another installed model in GeoIntel." Type="Variable" Display="always" Required="true" Mask="false">qwen3.5:9b</Config>
|
||||||
<Config Name="Ollama Timeout Seconds" Target="OLLAMA_TIMEOUT_SECONDS" Default="120" Mode="" Description="Maximum wait for one local assistant response." Type="Variable" Display="advanced" Required="true" Mask="false">120</Config>
|
<Config Name="Ollama Timeout Seconds" Target="OLLAMA_TIMEOUT_SECONDS" Default="120" Mode="" Description="Maximum wait for one local assistant response." Type="Variable" Display="advanced" Required="true" Mask="false">120</Config>
|
||||||
|
<Config Name="Ollama Context Tokens" Target="OLLAMA_CONTEXT_TOKENS" Default="16384" Mode="" Description="Context window reserved for grounded GIS measurements and the generated answer." Type="Variable" Display="advanced" Required="true" Mask="false">16384</Config>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -56,3 +56,4 @@ OLLAMA_BASE_URL=http://host.docker.internal:11434
|
|||||||
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
|
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
|
||||||
OLLAMA_TIMEOUT_SECONDS=120
|
OLLAMA_TIMEOUT_SECONDS=120
|
||||||
OLLAMA_MAX_OUTPUT_TOKENS=700
|
OLLAMA_MAX_OUTPUT_TOKENS=700
|
||||||
|
OLLAMA_CONTEXT_TOKENS=16384
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ OLLAMA_BASE_URL="${OLLAMA_BASE_URL:-http://host.docker.internal:11434}"
|
|||||||
OLLAMA_DEFAULT_MODEL="${OLLAMA_DEFAULT_MODEL:-qwen3.5:9b}"
|
OLLAMA_DEFAULT_MODEL="${OLLAMA_DEFAULT_MODEL:-qwen3.5:9b}"
|
||||||
OLLAMA_TIMEOUT_SECONDS="${OLLAMA_TIMEOUT_SECONDS:-120}"
|
OLLAMA_TIMEOUT_SECONDS="${OLLAMA_TIMEOUT_SECONDS:-120}"
|
||||||
OLLAMA_MAX_OUTPUT_TOKENS="${OLLAMA_MAX_OUTPUT_TOKENS:-700}"
|
OLLAMA_MAX_OUTPUT_TOKENS="${OLLAMA_MAX_OUTPUT_TOKENS:-700}"
|
||||||
|
OLLAMA_CONTEXT_TOKENS="${OLLAMA_CONTEXT_TOKENS:-16384}"
|
||||||
|
|
||||||
install_dockerman_metadata() {
|
install_dockerman_metadata() {
|
||||||
if [ -d /boot/config/plugins/dockerMan ]; then
|
if [ -d /boot/config/plugins/dockerMan ]; then
|
||||||
@@ -120,6 +121,7 @@ docker run -d \
|
|||||||
-e OLLAMA_DEFAULT_MODEL="$OLLAMA_DEFAULT_MODEL" \
|
-e OLLAMA_DEFAULT_MODEL="$OLLAMA_DEFAULT_MODEL" \
|
||||||
-e OLLAMA_TIMEOUT_SECONDS="$OLLAMA_TIMEOUT_SECONDS" \
|
-e OLLAMA_TIMEOUT_SECONDS="$OLLAMA_TIMEOUT_SECONDS" \
|
||||||
-e OLLAMA_MAX_OUTPUT_TOKENS="$OLLAMA_MAX_OUTPUT_TOKENS" \
|
-e OLLAMA_MAX_OUTPUT_TOKENS="$OLLAMA_MAX_OUTPUT_TOKENS" \
|
||||||
|
-e OLLAMA_CONTEXT_TOKENS="$OLLAMA_CONTEXT_TOKENS" \
|
||||||
-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data" \
|
-v "${GEOINTEL_POSTGIS_DATA_PATH}:/var/lib/postgresql/data" \
|
||||||
-v "${GEOINTEL_STORAGE_PATH}:/app/storage" \
|
-v "${GEOINTEL_STORAGE_PATH}:/app/storage" \
|
||||||
-v "${GEOINTEL_MODELS_PATH}:/app/models" \
|
-v "${GEOINTEL_MODELS_PATH}:/app/models" \
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ services:
|
|||||||
OLLAMA_DEFAULT_MODEL: ${OLLAMA_DEFAULT_MODEL:-qwen3.5:9b}
|
OLLAMA_DEFAULT_MODEL: ${OLLAMA_DEFAULT_MODEL:-qwen3.5:9b}
|
||||||
OLLAMA_TIMEOUT_SECONDS: ${OLLAMA_TIMEOUT_SECONDS:-120}
|
OLLAMA_TIMEOUT_SECONDS: ${OLLAMA_TIMEOUT_SECONDS:-120}
|
||||||
OLLAMA_MAX_OUTPUT_TOKENS: ${OLLAMA_MAX_OUTPUT_TOKENS:-700}
|
OLLAMA_MAX_OUTPUT_TOKENS: ${OLLAMA_MAX_OUTPUT_TOKENS:-700}
|
||||||
|
OLLAMA_CONTEXT_TOKENS: ${OLLAMA_CONTEXT_TOKENS:-16384}
|
||||||
ports:
|
ports:
|
||||||
- "${GEOINTEL_FRONTEND_PORT:-1202}:80"
|
- "${GEOINTEL_FRONTEND_PORT:-1202}:80"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ services:
|
|||||||
OLLAMA_DEFAULT_MODEL: ${OLLAMA_DEFAULT_MODEL:-qwen3.5:9b}
|
OLLAMA_DEFAULT_MODEL: ${OLLAMA_DEFAULT_MODEL:-qwen3.5:9b}
|
||||||
OLLAMA_TIMEOUT_SECONDS: ${OLLAMA_TIMEOUT_SECONDS:-120}
|
OLLAMA_TIMEOUT_SECONDS: ${OLLAMA_TIMEOUT_SECONDS:-120}
|
||||||
OLLAMA_MAX_OUTPUT_TOKENS: ${OLLAMA_MAX_OUTPUT_TOKENS:-700}
|
OLLAMA_MAX_OUTPUT_TOKENS: ${OLLAMA_MAX_OUTPUT_TOKENS:-700}
|
||||||
|
OLLAMA_CONTEXT_TOKENS: ${OLLAMA_CONTEXT_TOKENS:-16384}
|
||||||
ports:
|
ports:
|
||||||
- "${GEOINTEL_BACKEND_PORT:-8000}:8000"
|
- "${GEOINTEL_BACKEND_PORT:-8000}:8000"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1637,4 +1637,7 @@ metrics from PostGIS and includes dated observations only for persisted
|
|||||||
temporal series. Geometry is not sent to Ollama. The response contains the
|
temporal series. Geometry is not sent to Ollama. The response contains the
|
||||||
answer, used model, scope label, context metrics, discovered temporal series,
|
answer, used model, scope label, context metrics, discovered temporal series,
|
||||||
source dataset ids and warnings. Missing measurements remain unavailable;
|
source dataset ids and warnings. Missing measurements remain unavailable;
|
||||||
specifically, no water volume is inferred from 2D water geometry.
|
specifically, no water volume is inferred from 2D water geometry. The backend
|
||||||
|
sets an explicit Ollama context window and returns
|
||||||
|
`OLLAMA_RESPONSE_TRUNCATED` instead of accepting a response with
|
||||||
|
`done_reason=length` as a complete answer.
|
||||||
|
|||||||
@@ -8389,7 +8389,7 @@ Validation evidence:
|
|||||||
- Focused temporal, source, Ollama, navigation and Unraid regression tests
|
- Focused temporal, source, Ollama, navigation and Unraid regression tests
|
||||||
passed, including direct coverage for multi-metric history and DockerMan host
|
passed, including direct coverage for multi-metric history and DockerMan host
|
||||||
mapping.
|
mapping.
|
||||||
- Full readiness passed 615 backend tests, backend compilation, 91 documented
|
- Full readiness passed 617 backend tests, backend compilation, 91 documented
|
||||||
API routes, one Alembic head, frontend TypeScript typecheck/build and all
|
API routes, one Alembic head, frontend TypeScript typecheck/build and all
|
||||||
shell syntax gates.
|
shell syntax gates.
|
||||||
- Tower deployment passed the all-in-one container health check, live PostGIS
|
- Tower deployment passed the all-in-one container health check, live PostGIS
|
||||||
@@ -8403,6 +8403,15 @@ Validation evidence:
|
|||||||
message keys now use a session-local monotonic id generator; no persisted or
|
message keys now use a session-local monotonic id generator; no persisted or
|
||||||
security-sensitive identity depends on it. Typecheck, production build and
|
security-sensitive identity depends on it. Typecheck, production build and
|
||||||
full readiness reran successfully.
|
full readiness reran successfully.
|
||||||
|
- The regional operator then reused the five governed forest snapshots and
|
||||||
|
imported 15 new authoritative snapshots: water, built functions and
|
||||||
|
transport for 2013, 2016, 2019, 2022 and 2025. A live exact-Area Mol water
|
||||||
|
comparison returned 606.16 ha in 2013 and 641.75 ha in 2025 across all five
|
||||||
|
observations; the API retained the 10 m raster and object-lineage warnings.
|
||||||
|
- Browser QA also reproduced an Ollama `done_reason=length`: the previous
|
||||||
|
implicit 4096-token context left only 86 tokens after a 4010-token grounded
|
||||||
|
prompt. GeoIntel now requests a configurable 16,384-token context and rejects
|
||||||
|
any future length-truncated response instead of presenting a partial answer.
|
||||||
|
|
||||||
Known limitations:
|
Known limitations:
|
||||||
- Water volume remains unavailable until a governed depth/bathymetry source is
|
- Water volume remains unavailable until a governed depth/bathymetry source is
|
||||||
|
|||||||
Reference in New Issue
Block a user