From e24a366a9ecae4771a8a9f2dcca30e82e26ca98f Mon Sep 17 00:00:00 2001 From: Peter Jausovec Date: Sun, 20 Apr 2025 16:29:47 -0700 Subject: [PATCH 1/2] fix: ollama fails when tools use optional args Signed-off-by: Peter Jausovec --- .../models/ollama/_ollama_client.py | 11 ++++- .../test_ollama_chat_completion_client.py | 40 +++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/python/packages/autogen-ext/src/autogen_ext/models/ollama/_ollama_client.py b/python/packages/autogen-ext/src/autogen_ext/models/ollama/_ollama_client.py index e8abf67614a9..0e3e16f40cf8 100644 --- a/python/packages/autogen-ext/src/autogen_ext/models/ollama/_ollama_client.py +++ b/python/packages/autogen-ext/src/autogen_ext/models/ollama/_ollama_client.py @@ -315,8 +315,17 @@ def convert_tools( if parameters is not None: ollama_properties = {} for prop_name, prop_schema in parameters["properties"].items(): + # Determine property type, checking "type" first, then "anyOf", defaulting to "string" + prop_type = prop_schema.get("type") + if prop_type is None and "anyOf" in prop_schema: + prop_type = next( + (opt.get("type") for opt in prop_schema["anyOf"] if opt.get("type") != "null"), + None # Default to None if no non-null type found in anyOf + ) + prop_type = prop_type or "string" + ollama_properties[prop_name] = OllamaTool.Function.Parameters.Property( - type=prop_schema["type"], + type=prop_type, description=prop_schema["description"] if "description" in prop_schema else None, ) result.append( diff --git a/python/packages/autogen-ext/tests/models/test_ollama_chat_completion_client.py b/python/packages/autogen-ext/tests/models/test_ollama_chat_completion_client.py index 7499e31beec3..eebb912cbe89 100644 --- a/python/packages/autogen-ext/tests/models/test_ollama_chat_completion_client.py +++ b/python/packages/autogen-ext/tests/models/test_ollama_chat_completion_client.py @@ -1,6 +1,6 @@ import json import logging -from typing import Any, AsyncGenerator, Dict, List, Mapping +from typing import Any, AsyncGenerator, Dict, List, Mapping, Optional import httpx import pytest @@ -14,12 +14,13 @@ UserMessage, ) from autogen_core.tools import FunctionTool -from autogen_ext.models.ollama import OllamaChatCompletionClient -from autogen_ext.models.ollama._ollama_client import OLLAMA_VALID_CREATE_KWARGS_KEYS from httpx import Response from ollama import AsyncClient, ChatResponse, Message from pydantic import BaseModel +from autogen_ext.models.ollama import OllamaChatCompletionClient +from autogen_ext.models.ollama._ollama_client import OLLAMA_VALID_CREATE_KWARGS_KEYS, convert_tools + def _mock_request(*args: Any, **kwargs: Any) -> Response: return Response(status_code=200, content="{'response': 'Hello world!'}") @@ -206,6 +207,39 @@ async def _mock_chat(*args: Any, **kwargs: Any) -> ChatResponse: assert create_result.usage.completion_tokens == 12 +@pytest.mark.asyncio +async def test_convert_tools() -> None: + def add(x: int, y: Optional[int]) -> str: + return str(x + y) + + add_tool = FunctionTool(add, description="Add two numbers") + + + tool_schema_noparam = { + "name": "manual_tool", + "description": "A tool defined manually", + "parameters": { + "type": "object", + "properties": { + "param_with_type": {"type": "integer", "description": "An integer param"}, + "param_without_type": {"description": "A param without explicit type"}, + }, + "required": ["param_with_type"], + }, + } + + converted_tools = convert_tools([add_tool, tool_schema_noparam]) + assert len(converted_tools) == 2 + assert converted_tools[0].function.name == add_tool.name + assert converted_tools[0].function.parameters.properties["y"].type == "integer" + + # test it defaults to string + assert converted_tools[1].function.name == "manual_tool" + assert converted_tools[1].function.parameters.properties["param_with_type"].type == "integer" + assert converted_tools[1].function.parameters.properties["param_without_type"].type == "string" + assert converted_tools[1].function.parameters.required == ["param_with_type"] + + @pytest.mark.asyncio async def test_create_stream_tools(monkeypatch: pytest.MonkeyPatch) -> None: def add(x: int, y: int) -> str: From f9c5d76f636a9797a221361607d9362ef63d8a77 Mon Sep 17 00:00:00 2001 From: Eric Zhu Date: Mon, 21 Apr 2025 16:16:07 -0700 Subject: [PATCH 2/2] fix types --- .../models/ollama/_ollama_client.py | 2 +- .../test_ollama_chat_completion_client.py | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/python/packages/autogen-ext/src/autogen_ext/models/ollama/_ollama_client.py b/python/packages/autogen-ext/src/autogen_ext/models/ollama/_ollama_client.py index 0e3e16f40cf8..83699bbe548a 100644 --- a/python/packages/autogen-ext/src/autogen_ext/models/ollama/_ollama_client.py +++ b/python/packages/autogen-ext/src/autogen_ext/models/ollama/_ollama_client.py @@ -320,7 +320,7 @@ def convert_tools( if prop_type is None and "anyOf" in prop_schema: prop_type = next( (opt.get("type") for opt in prop_schema["anyOf"] if opt.get("type") != "null"), - None # Default to None if no non-null type found in anyOf + None, # Default to None if no non-null type found in anyOf ) prop_type = prop_type or "string" diff --git a/python/packages/autogen-ext/tests/models/test_ollama_chat_completion_client.py b/python/packages/autogen-ext/tests/models/test_ollama_chat_completion_client.py index eebb912cbe89..c020c6f932e0 100644 --- a/python/packages/autogen-ext/tests/models/test_ollama_chat_completion_client.py +++ b/python/packages/autogen-ext/tests/models/test_ollama_chat_completion_client.py @@ -13,13 +13,12 @@ FunctionExecutionResultMessage, UserMessage, ) -from autogen_core.tools import FunctionTool -from httpx import Response -from ollama import AsyncClient, ChatResponse, Message -from pydantic import BaseModel - +from autogen_core.tools import FunctionTool, ToolSchema from autogen_ext.models.ollama import OllamaChatCompletionClient from autogen_ext.models.ollama._ollama_client import OLLAMA_VALID_CREATE_KWARGS_KEYS, convert_tools +from httpx import Response +from ollama import AsyncClient, ChatResponse, Message, Tool +from pydantic import BaseModel def _mock_request(*args: Any, **kwargs: Any) -> Response: @@ -210,12 +209,13 @@ async def _mock_chat(*args: Any, **kwargs: Any) -> ChatResponse: @pytest.mark.asyncio async def test_convert_tools() -> None: def add(x: int, y: Optional[int]) -> str: + if y is None: + return str(x) return str(x + y) add_tool = FunctionTool(add, description="Add two numbers") - - tool_schema_noparam = { + tool_schema_noparam: ToolSchema = { "name": "manual_tool", "description": "A tool defined manually", "parameters": { @@ -230,10 +230,16 @@ def add(x: int, y: Optional[int]) -> str: converted_tools = convert_tools([add_tool, tool_schema_noparam]) assert len(converted_tools) == 2 + assert isinstance(converted_tools[0].function, Tool.Function) + assert isinstance(converted_tools[0].function.parameters, Tool.Function.Parameters) + assert converted_tools[0].function.parameters.properties is not None assert converted_tools[0].function.name == add_tool.name assert converted_tools[0].function.parameters.properties["y"].type == "integer" # test it defaults to string + assert isinstance(converted_tools[1].function, Tool.Function) + assert isinstance(converted_tools[1].function.parameters, Tool.Function.Parameters) + assert converted_tools[1].function.parameters.properties is not None assert converted_tools[1].function.name == "manual_tool" assert converted_tools[1].function.parameters.properties["param_with_type"].type == "integer" assert converted_tools[1].function.parameters.properties["param_without_type"].type == "string"