fix: prevent truncated Ollama answers
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-15 07:39:03 +02:00
parent 5be8f628ef
commit 56f823f02a
13 changed files with 89 additions and 4 deletions
+4 -1
View File
@@ -1100,6 +1100,7 @@ OLLAMA_BASE_URL=http://host.docker.internal:11434
OLLAMA_DEFAULT_MODEL=qwen3.5:9b
OLLAMA_TIMEOUT_SECONDS=120
OLLAMA_MAX_OUTPUT_TOKENS=700
OLLAMA_CONTEXT_TOKENS=16384
```
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
`POST /api/v1/projects/{project_id}/assistant/query`. A requested model must be
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
+1
View File
@@ -51,6 +51,7 @@ class Settings(BaseSettings):
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_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(
default=["http://localhost:5173", "http://127.0.0.1:5173"],
validation_alias="CORS_ORIGINS",
+15 -1
View File
@@ -367,9 +367,23 @@ class GeoAssistantService:
"stream": False,
"think": False,
"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 {}
answer = str(message.get("content") or "").strip()
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["payload"]["stream"] 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 "scope.label is het exact geanalyseerde gebied" 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:
earlier = {
"metrics": [