Skip to content

Commit 25e5cf4

Browse files
committed
test(brains/ollama): cover tools forwarding and tool-call surfacing
Three cases over the function-calling path that previously had no coverage: tools land in the request body when passed (and the key is omitted otherwise), and message.tool_calls with dict arguments come back as tool_call_deltas with string arguments plus a normalized tool_calls finish reason.
1 parent 959fbbe commit 25e5cf4

1 file changed

Lines changed: 68 additions & 0 deletions

File tree

tests/test_brains_ollama.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,74 @@ async def test_stream_skips_empty_and_malformed_lines():
121121
assert out[-1].done is True
122122

123123

124+
@pytest.mark.asyncio
125+
async def test_stream_forwards_tools_to_request_body():
126+
"""Ollama's /api/chat supports a top-level `tools` field. The adapter must
127+
forward declared tools (like its sibling adapters) — otherwise the model is
128+
never told they exist and can never emit a tool call."""
129+
tools = [{
130+
"type": "function",
131+
"function": {"name": "get_weather", "parameters": {"type": "object"}},
132+
}]
133+
fake = _FakeAsyncClient([
134+
json.dumps({"message": {"content": "hi"}, "done": True, "done_reason": "stop"}),
135+
])
136+
adapter = OllamaAdapter(base_url="http://x", model="llama3.2", http=fake)
137+
async for _ in adapter.stream([{"role": "user", "content": "weather?"}], tools=tools):
138+
pass
139+
assert fake.last_call["json"]["tools"] == tools
140+
141+
142+
@pytest.mark.asyncio
143+
async def test_stream_omits_tools_when_none():
144+
"""No tools passed → no `tools` key in the body (don't send an empty field)."""
145+
fake = _FakeAsyncClient([
146+
json.dumps({"message": {"content": "hi"}, "done": True, "done_reason": "stop"}),
147+
])
148+
adapter = OllamaAdapter(base_url="http://x", model="llama3.2", http=fake)
149+
async for _ in adapter.stream([{"role": "user", "content": "hi"}]):
150+
pass
151+
assert "tools" not in fake.last_call["json"]
152+
153+
154+
@pytest.mark.asyncio
155+
async def test_stream_surfaces_tool_calls_as_deltas():
156+
"""Ollama returns tool calls in `message.tool_calls` with a dict of
157+
arguments. The adapter must re-shape each into a hub-shaped tool_call_delta
158+
(string arguments) and report finish_reason='tool_calls' so the hub's
159+
auto-resolution fires — dropping them left an Ollama AI unable to call tools."""
160+
lines = [
161+
json.dumps({"message": {
162+
"role": "assistant",
163+
"content": "",
164+
"tool_calls": [
165+
{"function": {"name": "get_weather", "arguments": {"city": "SF"}}},
166+
{"function": {"name": "get_time", "arguments": {"tz": "PST"}}},
167+
],
168+
}, "done": True, "done_reason": "stop"}),
169+
]
170+
fake = _FakeAsyncClient(lines)
171+
adapter = OllamaAdapter(base_url="http://x", model="llama3.2", http=fake)
172+
out: list[ChatChunk] = []
173+
async for chunk in adapter.stream([{"role": "user", "content": "weather + time?"}]):
174+
out.append(chunk)
175+
176+
tcds = [c.tool_call_delta for c in out if c.tool_call_delta]
177+
assert len(tcds) == 2
178+
assert tcds[0]["index"] == 0
179+
assert tcds[0]["type"] == "function"
180+
assert tcds[0]["id"] # synthesized, non-empty
181+
assert tcds[0]["function"]["name"] == "get_weather"
182+
# arguments must be a JSON string, not a dict (the hub concatenates them)
183+
assert tcds[0]["function"]["arguments"] == json.dumps({"city": "SF"})
184+
assert tcds[1]["index"] == 1
185+
assert tcds[1]["function"]["name"] == "get_time"
186+
187+
# the turn ended in a tool call → finish_reason normalized to tool_calls
188+
assert out[-1].done is True
189+
assert out[-1].finish_reason == "tool_calls"
190+
191+
124192
@pytest.mark.asyncio
125193
async def test_stream_raises_on_upstream_error():
126194
"""A non-2xx response (e.g. 404 unknown model) returns a JSON error body,

0 commit comments

Comments
 (0)