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
9 changes: 6 additions & 3 deletions ollama/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,26 @@ def _parse_docstring(doc_string: Union[str, None]) -> dict[str, str]:


def convert_function_to_tool(func: Callable) -> Tool:
signature = inspect.signature(func)
doc_string_hash = str(hash(inspect.getdoc(func)))
parsed_docstring = _parse_docstring(inspect.getdoc(func))
schema = type(
func.__name__,
(pydantic.BaseModel,),
{
'__annotations__': {k: v.annotation if v.annotation != inspect._empty else str for k, v in inspect.signature(func).parameters.items()},
'__signature__': inspect.signature(func),
'__annotations__': {k: v.annotation if v.annotation != inspect._empty else str for k, v in signature.parameters.items()},
'__signature__': signature,
'__doc__': parsed_docstring[doc_string_hash],
**{k: v.default for k, v in signature.parameters.items() if v.default is not inspect.Parameter.empty},
},
).model_json_schema()

for k, v in schema.get('properties', {}).items():
# If type is missing, the default is string
types = {t.get('type', 'string') for t in v.get('anyOf')} if 'anyOf' in v else {v.get('type', 'string')}
if 'null' in types:
schema['required'].remove(k)
if k in schema.get('required', []):
schema['required'].remove(k)
types.discard('null')

schema['properties'][k] = {
Expand Down
42 changes: 42 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,52 @@
import json
import sys
from enum import IntEnum
from typing import Dict, List, Mapping, Sequence, Set, Tuple, Union

from ollama._utils import convert_function_to_tool


def test_function_defaults_are_not_required():
def search(query: str, limit: int = 5):
return query, limit

parameters = convert_function_to_tool(search).function.parameters
assert parameters.required == ['query']
assert parameters.properties['limit'].type == 'integer'


def test_function_default_with_custom_comparison():
class DefaultLimit(IntEnum):
FIVE = 5

def __ne__(self, other):
raise RuntimeError('Default value comparison is not supported')

def search(query: str, limit: int = DefaultLimit.FIVE):
return query, limit

parameters = convert_function_to_tool(search).function.parameters
assert parameters.required == ['query']
assert parameters.properties['limit'].type == 'integer'


def test_nullable_default_with_no_required_arguments():
def search(query: Union[str, None] = None):
return query

parameters = convert_function_to_tool(search).function.parameters
assert not parameters.required
assert parameters.properties['query'].type == 'string'


def test_keyword_only_defaults_are_not_required():
def search(query: str, *, limit: int = 5):
return query, limit

parameters = convert_function_to_tool(search).function.parameters
assert parameters.required == ['query']


def test_function_to_tool_conversion():
def add_numbers(x: int, y: Union[int, None] = None) -> int:
"""Add two numbers together.
Expand Down