Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 61 additions & 9 deletions python/packages/kagent-adk/src/kagent/adk/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
import math
from typing import Any, Callable, Optional, Union

import httpx
Expand Down Expand Up @@ -167,6 +168,45 @@ def header_provider(readonly_context: Optional[ReadonlyContext]) -> dict[str, st
return header_provider


_BOOL_TRUE = {"1", "true", "yes", "on"}
_BOOL_FALSE = {"0", "false", "no", "off"}


def _coerce_option_bool(value: str) -> bool:
"""Parse a CRD string into a bool, accepting the common truthy/falsy spellings.

The previous implementation treated only the exact string ``"true"`` as True,
so ``"1"``, ``"yes"`` or ``"True"`` silently became False. Anything outside the
recognized spellings raises ValueError so the caller can fall back and warn.
"""
normalized = value.strip().lower()
if normalized in _BOOL_TRUE:
return True
if normalized in _BOOL_FALSE:
return False
raise ValueError(f"invalid boolean option value: {value!r}")


def _coerce_option_int(value: str) -> int:
"""Parse a CRD string into an int.

Accepts a plain integer ("2048") and a float that is finite and exactly
integral ("2048.0"), which JSON/YAML tooling can produce for an int-typed
option. Rejects a non-integral float ("2048.5") rather than truncating it,
and rejects "inf"/"nan", so the caller falls back and warns instead of
silently losing precision or raising OverflowError.
"""
stripped = value.strip()
try:
return int(stripped)
except ValueError:
pass
number = float(stripped)
if not math.isfinite(number) or not number.is_integer():
raise ValueError(f"not an integer option value: {value!r}")
return int(number)


def _convert_ollama_options(options: dict[str, str] | None) -> dict[str, Any]:
"""Convert Ollama options from string values to their correct types.

Expand Down Expand Up @@ -204,15 +244,27 @@ def _convert_ollama_options(options: dict[str, str] | None) -> dict[str, Any]:
else:
target_type = annotation

# Convert based on the target type
if target_type is int:
converted[key] = int(value)
elif target_type is float:
converted[key] = float(value)
elif target_type is bool:
converted[key] = value.lower() == "true"
else:
# Keep as string for other types (e.g., Sequence[str])
# Convert based on the target type. Values come from CRD strings, so
# coercion is tolerant: a malformed value is kept as a string and
# left for the Ollama SDK to report, rather than raising here and
# failing the whole agent build.
try:
if target_type is bool:
converted[key] = _coerce_option_bool(value)
elif target_type is int:
converted[key] = _coerce_option_int(value)
elif target_type is float:
converted[key] = float(value)
else:
# Keep as string for other types (e.g., Sequence[str])
converted[key] = value
except (TypeError, ValueError, OverflowError):
logger.warning(
"Could not convert Ollama option %r=%r to %s; forwarding the raw value.",
key,
value,
getattr(target_type, "__name__", target_type),
)
converted[key] = value
else:
# Unknown option - keep as string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,69 @@ def test_create_llm_from_ollama_model_config(self):
# Options are type-coerced by _convert_ollama_options before reaching create_ollama_llm
assert result.ollama_options["temperature"] == 0.8
assert result.ollama_options["top_p"] == 0.9


class TestConvertOllamaOptions:
"""Coercion of CRD string options into the Ollama SDK's typed values."""

def test_int_option_coerced(self):
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options({"num_ctx": "64000"}) == {"num_ctx": 64000}

def test_int_option_accepts_integral_float_string(self):
"""A value like "64000.0" (which JSON/YAML tooling can produce for an int
field) must not crash the whole agent build."""
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options({"num_ctx": "64000.0"}) == {"num_ctx": 64000}

def test_float_option_coerced(self):
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options({"temperature": "0.7"}) == {"temperature": 0.7}

@pytest.mark.parametrize("value", ["1", "true", "True", "YES", "on", " true "])
def test_bool_option_truthy_spellings(self, value):
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options({"low_vram": value}) == {"low_vram": True}

@pytest.mark.parametrize("value", ["0", "false", "False", "no", "off"])
def test_bool_option_falsy_spellings(self, value):
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options({"low_vram": value}) == {"low_vram": False}

def test_unparseable_value_is_forwarded_not_raised(self):
"""A malformed value is kept as a string for the SDK to report, rather
than raising and failing agent construction."""
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options({"num_ctx": "not-a-number"}) == {"num_ctx": "not-a-number"}

def test_non_integral_float_is_not_truncated(self):
"""A fractional value for an int option is a mistake; forward it rather
than silently truncating "2048.5" to 2048."""
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options({"num_ctx": "2048.5"}) == {"num_ctx": "2048.5"}

@pytest.mark.parametrize("value", ["inf", "-inf", "nan", "1e400"])
def test_non_finite_int_value_is_forwarded(self, value):
"""int(float("inf")) raises OverflowError, so these must fall back to the
raw value instead of crashing agent construction."""
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options({"num_ctx": value}) == {"num_ctx": value}

def test_unknown_option_kept_as_string(self):
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options({"custom_flag": "value"}) == {"custom_flag": "value"}

def test_empty_and_none(self):
from kagent.adk.types import _convert_ollama_options

assert _convert_ollama_options(None) == {}
assert _convert_ollama_options({}) == {}