Skip to content

Commit ed3ba8a

Browse files
committed
Code style updates
1 parent 0d9eec0 commit ed3ba8a

File tree

4 files changed

+65
-68
lines changed

4 files changed

+65
-68
lines changed

ollama/_client.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,14 @@ def chat(
297297
"""
298298
Create a chat response using the requested model.
299299
300+
Args:
301+
tools (Sequence[Union[Mapping[str, Any], Tool, Callable]]):
302+
A JSON schema as a dict, an Ollama Tool or a Python Function.
303+
Python functions need to follow Google style docstrings to be converted to an Ollama Tool.
304+
For more information, see: https://google.github.io/styleguide/pyguide.html#38-Docstrings
305+
stream (bool): Whether to stream the response.
306+
format (Optional[Literal['', 'json']]): The format of the response.
307+
300308
Raises `RequestError` if a model is not provided.
301309
302310
Raises `ResponseError` if the request could not be fulfilled.
@@ -1084,10 +1092,7 @@ def _copy_tools(tools: Optional[Sequence[Union[Mapping[str, Any], Tool, Callable
10841092
return []
10851093

10861094
for unprocessed_tool in tools:
1087-
if callable(unprocessed_tool):
1088-
yield convert_function_to_tool(unprocessed_tool)
1089-
else:
1090-
yield Tool.model_validate(unprocessed_tool)
1095+
yield convert_function_to_tool(unprocessed_tool) if callable(unprocessed_tool) else Tool.model_validate(unprocessed_tool)
10911096

10921097

10931098
def _as_path(s: Optional[Union[str, PathLike]]) -> Union[Path, None]:
@@ -1162,6 +1167,8 @@ def _parse_host(host: Optional[str]) -> str:
11621167
'https://[0001:002:003:0004::1]:56789/path'
11631168
>>> _parse_host('[0001:002:003:0004::1]:56789/path/')
11641169
'http://[0001:002:003:0004::1]:56789/path'
1170+
>>> _parse_host('http://host.docker.internal:11434/path')
1171+
'http://host.docker.internal:11434/path'
11651172
"""
11661173

11671174
host, port = host or '', 11434

ollama/_types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ class ModelDetails(SubscriptableBaseModel):
336336

337337
class ListResponse(SubscriptableBaseModel):
338338
class Model(SubscriptableBaseModel):
339-
name: Optional[str] = None
339+
model: Optional[str] = None
340340
modified_at: Optional[datetime] = None
341341
digest: Optional[str] = None
342342
size: Optional[ByteSize] = None

ollama/_utils.py

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from __future__ import annotations
2+
from collections import defaultdict
23
import inspect
34
from typing import Callable, Union
45

@@ -7,96 +8,73 @@
78

89

910
def _parse_docstring(doc_string: Union[str, None]) -> dict[str, str]:
10-
parsed_docstring = {'description': ''}
11+
parsed_docstring = defaultdict(str)
1112
if not doc_string:
1213
return parsed_docstring
1314

1415
lowered_doc_string = doc_string.lower()
1516

16-
if 'args:' not in lowered_doc_string:
17-
parsed_docstring['description'] = lowered_doc_string.strip()
18-
return parsed_docstring
19-
20-
else:
21-
parsed_docstring['description'] = lowered_doc_string.split('args:')[0].strip()
22-
args_section = lowered_doc_string.split('args:')[1]
23-
24-
if 'returns:' in lowered_doc_string:
25-
# Return section can be captured and used
26-
args_section = args_section.split('returns:')[0]
17+
key = hash(doc_string)
18+
parsed_docstring[key] = ''
19+
for line in lowered_doc_string.splitlines():
20+
if line.startswith('args:'):
21+
key = 'args'
22+
elif line.startswith('returns:') or line.startswith('yields:') or line.startswith('raises:'):
23+
key = '_'
2724

28-
if 'yields:' in lowered_doc_string:
29-
args_section = args_section.split('yields:')[0]
25+
else:
26+
# maybe change to a list and join later
27+
parsed_docstring[key] += f'{line.strip()}\n'
3028

31-
cur_var = None
32-
for line in args_section.split('\n'):
29+
last_key = None
30+
for line in parsed_docstring['args'].splitlines():
3331
line = line.strip()
34-
if not line:
35-
continue
36-
if ':' not in line:
37-
# Continuation of the previous parameter's description
38-
if cur_var:
39-
parsed_docstring[cur_var] += f' {line}'
40-
continue
41-
42-
# For the case with: `param_name (type)`: ...
43-
if '(' in line:
44-
param_name = line.split('(')[0]
45-
param_desc = line.split('):')[1]
46-
47-
# For the case with: `param_name: ...`
48-
else:
49-
param_name, param_desc = line.split(':', 1)
32+
if ':' in line and not line.startswith('args'):
33+
# Split on first occurrence of '(' or ':' to separate arg name from description
34+
split_char = '(' if '(' in line else ':'
35+
arg_name, rest = line.split(split_char, 1)
5036

51-
parsed_docstring[param_name.strip()] = param_desc.strip()
52-
cur_var = param_name.strip()
37+
last_key = arg_name.strip()
38+
# Get description after the colon
39+
arg_description = rest.split(':', 1)[1].strip() if split_char == '(' else rest.strip()
40+
parsed_docstring[last_key] = arg_description
41+
42+
elif last_key and line:
43+
parsed_docstring[last_key] += ' ' + line
5344

5445
return parsed_docstring
5546

5647

5748
def convert_function_to_tool(func: Callable) -> Tool:
49+
doc_string_hash = hash(inspect.getdoc(func))
50+
parsed_docstring = _parse_docstring(inspect.getdoc(func))
5851
schema = type(
5952
func.__name__,
6053
(pydantic.BaseModel,),
6154
{
62-
'__annotations__': {k: v.annotation for k, v in inspect.signature(func).parameters.items()},
55+
'__annotations__': {k: v.annotation if v.annotation != inspect._empty else str for k, v in inspect.signature(func).parameters.items()},
6356
'__signature__': inspect.signature(func),
64-
'__doc__': inspect.getdoc(func),
57+
'__doc__': parsed_docstring[doc_string_hash],
6558
},
6659
).model_json_schema()
6760

68-
properties = {}
69-
required = []
70-
parsed_docstring = _parse_docstring(schema.get('description'))
7161
for k, v in schema.get('properties', {}).items():
72-
prop = {
73-
'description': parsed_docstring.get(k, ''),
74-
'type': v.get('type'),
62+
# If type is missing, the default is string
63+
types = {t.get('type', 'string') for t in v.get('anyOf')} if 'anyOf' in v else {v.get('type', 'string')}
64+
if 'null' in types:
65+
schema['required'].remove(k)
66+
types.discard('null')
67+
68+
schema['properties'][k] = {
69+
'description': parsed_docstring[k],
70+
'type': ', '.join(types),
7571
}
7672

77-
if 'anyOf' in v:
78-
is_optional = any(t.get('type') == 'null' for t in v['anyOf'])
79-
types = [t.get('type', 'string') for t in v['anyOf'] if t.get('type') != 'null']
80-
prop['type'] = types[0] if len(types) == 1 else str(types)
81-
if not is_optional:
82-
required.append(k)
83-
else:
84-
if prop['type'] != 'null':
85-
required.append(k)
86-
87-
properties[k] = prop
88-
89-
schema['properties'] = properties
90-
9173
tool = Tool(
9274
function=Tool.Function(
9375
name=func.__name__,
94-
description=parsed_docstring.get('description'),
95-
parameters=Tool.Function.Parameters(
96-
type='object',
97-
properties=schema.get('properties', {}),
98-
required=required,
99-
),
76+
description=schema.get('description', ''),
77+
parameters=Tool.Function.Parameters(**schema),
10078
)
10179
)
10280

tests/test_utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def only_description():
188188

189189
tool = convert_function_to_tool(only_description).model_dump()
190190
assert tool['function']['description'] == 'a function with only a description.'
191-
assert tool['function']['parameters'] == {'type': 'object', 'properties': {}, 'required': []}
191+
assert tool['function']['parameters'] == {'type': 'object', 'properties': {}, 'required': None}
192192

193193
def only_description_with_args(x: int, y: int):
194194
"""
@@ -226,3 +226,15 @@ def function_with_yields(x: int, y: int):
226226
assert tool['function']['description'] == 'a function with yields section.'
227227
assert tool['function']['parameters']['properties']['x']['description'] == 'the first number'
228228
assert tool['function']['parameters']['properties']['y']['description'] == 'the second number'
229+
230+
231+
def test_function_with_no_types():
232+
def no_types(a, b):
233+
"""
234+
A function with no types.
235+
"""
236+
pass
237+
238+
tool = convert_function_to_tool(no_types).model_dump()
239+
assert tool['function']['parameters']['properties']['a']['type'] == 'string'
240+
assert tool['function']['parameters']['properties']['b']['type'] == 'string'

0 commit comments

Comments
 (0)