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:
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(

View File

@ -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
@ -13,11 +13,11 @@ from autogen_core.models import (
FunctionExecutionResultMessage,
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._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 ollama import AsyncClient, ChatResponse, Message
from ollama import AsyncClient, ChatResponse, Message, Tool
from pydantic import BaseModel
@ -206,6 +206,46 @@ async def test_create_tools(monkeypatch: pytest.MonkeyPatch) -> None:
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
async def test_create_stream_tools(monkeypatch: pytest.MonkeyPatch) -> None:
def add(x: int, y: int) -> str: