fix: ollama fails when tools use optional args (#6343)

## Why are these changes needed?
`convert_tools` failed if Optional args were used in tools (the `type`
field doesn't exist in that case and `anyOf` must be used).

This uses the `anyOf` field to pick the first non-null type to use.  

## Related issue number

Fixes #6323

## Checks

- [ ] I've included any doc changes needed for
<https://microsoft.github.io/autogen/>. See
<https://github.com/microsoft/autogen/blob/main/CONTRIBUTING.md> to
build and test documentation locally.
- [x] I've added tests (if relevant) corresponding to the changes
introduced in this PR.
- [x] I've made sure all auto checks have passed.

---------

Signed-off-by: Peter Jausovec <peter.jausovec@solo.io>
Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
Peter Jausovec 2025-04-21 17:06:46 -07:00 committed by GitHub
parent 89d77c77c5
commit d051da52c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 54 additions and 5 deletions

View File

@ -315,8 +315,17 @@ def convert_tools(
if parameters is not None: if parameters is not None:
ollama_properties = {} ollama_properties = {}
for prop_name, prop_schema in parameters["properties"].items(): 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( 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, description=prop_schema["description"] if "description" in prop_schema else None,
) )
result.append( result.append(

View File

@ -1,6 +1,6 @@
import json import json
import logging import logging
from typing import Any, AsyncGenerator, Dict, List, Mapping from typing import Any, AsyncGenerator, Dict, List, Mapping, Optional
import httpx import httpx
import pytest import pytest
@ -13,11 +13,11 @@ from autogen_core.models import (
FunctionExecutionResultMessage, FunctionExecutionResultMessage,
UserMessage, UserMessage,
) )
from autogen_core.tools import FunctionTool from autogen_core.tools import FunctionTool, ToolSchema
from autogen_ext.models.ollama import OllamaChatCompletionClient from autogen_ext.models.ollama import OllamaChatCompletionClient
from autogen_ext.models.ollama._ollama_client import OLLAMA_VALID_CREATE_KWARGS_KEYS from autogen_ext.models.ollama._ollama_client import OLLAMA_VALID_CREATE_KWARGS_KEYS, convert_tools
from httpx import Response from httpx import Response
from ollama import AsyncClient, ChatResponse, Message from ollama import AsyncClient, ChatResponse, Message, Tool
from pydantic import BaseModel from pydantic import BaseModel
@ -206,6 +206,46 @@ async def test_create_tools(monkeypatch: pytest.MonkeyPatch) -> None:
assert create_result.usage.completion_tokens == 12 assert create_result.usage.completion_tokens == 12
@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: ToolSchema = {
"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 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"
assert converted_tools[1].function.parameters.required == ["param_with_type"]
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_stream_tools(monkeypatch: pytest.MonkeyPatch) -> None: async def test_create_stream_tools(monkeypatch: pytest.MonkeyPatch) -> None:
def add(x: int, y: int) -> str: def add(x: int, y: int) -> str: