Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
13 changes: 7 additions & 6 deletions python/packages/ollama/agent_framework_ollama/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
Message,
ResponseStream,
UsageDetails,
validate_tool_mode,
)
from agent_framework._settings import load_settings
from agent_framework._telemetry import mark_feature_used
Expand Down Expand Up @@ -97,7 +98,7 @@ class OllamaChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], to
(converted to its JSON schema) for structured output.

# Options not supported in Ollama:
tool_choice: Ollama only supports auto tool choice.
tool_choice: Ollama only supports auto tool choice, but ``none`` is honored by omitting the tools.
allow_multiple_tool_calls: Not configurable.
user: Not supported.
store: Not supported.
Expand Down Expand Up @@ -216,9 +217,6 @@ class OllamaChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT], to
"""For thinking models: whether the model should think before responding."""

# ChatOptions fields not supported in Ollama
tool_choice: None # type: ignore[misc]
"""Not supported. Ollama only supports auto tool choice."""

allow_multiple_tool_calls: None # type: ignore[misc]
"""Not supported. Not configurable in Ollama."""

Expand Down Expand Up @@ -407,7 +405,7 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An
messages = prepend_instructions_to_messages(list(messages), instructions, role="system")

# Keys to exclude from processing
exclude_keys = {"instructions", "tool_choice"}
exclude_keys = {"instructions", "tool_choice", "tools"}

# Build run_options and model_options separately
run_options: dict[str, Any] = {}
Expand Down Expand Up @@ -453,8 +451,11 @@ def _prepare_options(self, messages: Sequence[Message], options: Mapping[str, An
run_options["model"] = self.model

# tools
# Ollama has no tool_choice parameter, so "none" is honored by not sending the tools at all.
tools = options.get("tools")
if tools is not None and (prepared_tools := self._prepare_tools_for_ollama(tools)):
tool_mode = validate_tool_mode(options.get("tool_choice"))
Comment thread
ankit2235 marked this conversation as resolved.
tools_disabled = tool_mode is not None and tool_mode.get("mode") == "none"
if tools is not None and not tools_disabled and (prepared_tools := self._prepare_tools_for_ollama(tools)):
run_options["tools"] = prepared_tools

return run_options
Expand Down
62 changes: 61 additions & 1 deletion python/packages/ollama/tests/test_ollama_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@
chat_middleware,
tool,
)
from agent_framework.exceptions import ChatClientException, ChatClientInvalidRequestException, SettingNotFoundError
from agent_framework.exceptions import (
ChatClientException,
ChatClientInvalidRequestException,
ContentError,
SettingNotFoundError,
)
from ollama import AsyncClient
from ollama._types import ChatResponse as OllamaChatResponse
from ollama._types import Message as OllamaMessage
Expand Down Expand Up @@ -907,3 +912,58 @@ def test_prepare_options_single_stop_string_becomes_list(ollama_unit_test_env: d
request = client._prepare_options(messages, {"stop": "END"})

assert request["options"]["stop"] == ["END"]


@pytest.mark.parametrize("tool_choice", ["none", {"mode": "none"}])
def test_prepare_options_tool_choice_none_omits_tools(ollama_unit_test_env: dict[str, str], tool_choice: Any) -> None:
Comment thread
ankit2235 marked this conversation as resolved.
"""Ollama has no tool_choice parameter, so "none" is honored by not offering the tools."""
client = OllamaChatClient()
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]

request = client._prepare_options(messages, {"tools": [hello_world], "tool_choice": tool_choice})

assert "tools" not in request
assert "tool_choice" not in request


@pytest.mark.parametrize("tool_choice", [None, "auto", "required"])
def test_prepare_options_other_tool_choices_keep_tools(ollama_unit_test_env: dict[str, str], tool_choice: Any) -> None:
"""Tool choices other than "none" still send the tools to Ollama."""
client = OllamaChatClient()
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]

request = client._prepare_options(messages, {"tools": [hello_world], "tool_choice": tool_choice})

assert request["tools"] == [hello_world.to_json_schema_spec()]
assert "tool_choice" not in request


@patch.object(AsyncClient, "chat", new_callable=AsyncMock)
async def test_cmc_function_invocation_limit_final_request_omits_tools(
mock_chat: AsyncMock,
ollama_unit_test_env: dict[str, str],
chat_history: list[Message],
mock_chat_completion_tool_call: OllamaChatResponse,
mock_chat_completion_response: OllamaChatResponse,
) -> None:
"""After the function invocation limit, the final request must not offer tools to Ollama."""
mock_chat.side_effect = [mock_chat_completion_tool_call, mock_chat_completion_response]
chat_history.append(Message(contents=["hello world"], role="user"))

ollama_client = OllamaChatClient()
ollama_client.function_invocation_configuration["max_iterations"] = 1
result = await ollama_client.get_response(messages=chat_history, options={"tools": [hello_world]})

assert mock_chat.call_count == 2
assert "tools" in mock_chat.call_args_list[0].kwargs
assert "tools" not in mock_chat.call_args_list[1].kwargs
assert result.text == "test"


def test_prepare_options_invalid_tool_choice_raises(ollama_unit_test_env: dict[str, str]) -> None:
"""An invalid tool_choice is rejected instead of being silently ignored."""
client = OllamaChatClient()
messages = [Message(role="user", contents=[Content.from_text(text="hello")])]

with pytest.raises(ContentError):
client._prepare_options(messages, {"tools": [hello_world], "tool_choice": "bogus"})
Loading