Skip to content

Commit 2530e36

Browse files
remove synthetic id fallback, key accumulator by index
1 parent af73962 commit 2530e36

2 files changed

Lines changed: 41 additions & 40 deletions

File tree

integrations/ollama/src/haystack_integrations/components/generators/ollama/chat/chat_generator.py

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,10 @@ def _convert_ollama_response_to_chatmessage(ollama_response: ChatResponse) -> Ch
165165
tool_calls: list[ToolCall] = []
166166

167167
if ollama_tool_calls := ollama_message.get("tool_calls"):
168-
for idx, ollama_tc in enumerate(ollama_tool_calls):
168+
for ollama_tc in ollama_tool_calls:
169169
tool_calls.append(
170170
ToolCall(
171-
id=ollama_tc.get("id") or f"call_{idx}",
171+
id=ollama_tc.get("id"),
172172
tool_name=ollama_tc["function"]["name"],
173173
arguments=ollama_tc["function"]["arguments"],
174174
)
@@ -209,7 +209,7 @@ def _build_chunk(
209209
tool_calls_list.append(
210210
ToolCallDelta(
211211
index=tool_call_index,
212-
id=tool_call.get("id") or f"call_{tool_call_index}",
212+
id=tool_call.get("id"),
213213
tool_name=tool_call["function"]["name"],
214214
arguments=json.dumps(tool_call["function"]["arguments"])
215215
if tool_call["function"]["arguments"]
@@ -372,10 +372,11 @@ def _handle_streaming_response(
372372
component_info = ComponentInfo.from_component(self)
373373
chunks: list[StreamingChunk] = []
374374

375-
# Accumulators
376-
arg_by_id: dict[str, str] = {}
377-
name_by_id: dict[str, str] = {}
378-
id_order: list[str] = []
375+
# Accumulators keyed by tool_call.index (always unique per call, even for repeated tool names)
376+
arg_by_index: dict[str, str] = {}
377+
name_by_index: dict[str, str] = {}
378+
id_by_index: dict[str, str | None] = {}
379+
index_order: list[str] = []
379380
tool_call_index: int = 0
380381

381382
# track reasoning and content blocks to correctly set start=True on the first chunk of each block
@@ -402,15 +403,14 @@ def _handle_streaming_response(
402403

403404
if chunk.tool_calls:
404405
for tool_call in chunk.tool_calls:
405-
# id is always set by _build_chunk (either from the server or synthetic "call_N").
406-
# Fall back to tool_name only as a last resort for callers that bypass _build_chunk.
407-
tool_call_id = tool_call.id or tool_call.tool_name or ""
406+
key = str(tool_call.index)
408407
args = tool_call.arguments or ""
409408

410-
if tool_call_id not in id_order:
411-
id_order.append(tool_call_id)
412-
name_by_id[tool_call_id] = tool_call.tool_name or ""
413-
arg_by_id[tool_call_id] = args
409+
if key not in index_order:
410+
index_order.append(key)
411+
name_by_index[key] = tool_call.tool_name or ""
412+
id_by_index[key] = tool_call.id
413+
arg_by_index[key] = args
414414

415415
if callback:
416416
callback(chunk)
@@ -423,10 +423,10 @@ def _handle_streaming_response(
423423
reasoning += c.reasoning.reasoning_text if c.reasoning else ""
424424

425425
tool_calls = []
426-
for tool_call_id in id_order:
427-
arguments: str = arg_by_id.get(tool_call_id, "")
426+
for key in index_order:
427+
arguments: str = arg_by_index.get(key, "")
428428
tool_calls.append(
429-
ToolCall(id=tool_call_id, tool_name=name_by_id[tool_call_id], arguments=json.loads(arguments))
429+
ToolCall(id=id_by_index[key], tool_name=name_by_index[key], arguments=json.loads(arguments))
430430
)
431431

432432
# We can't use _convert_streaming_chunks_to_chat_message because
@@ -453,10 +453,11 @@ async def _handle_streaming_response_async(
453453
component_info = ComponentInfo.from_component(self)
454454
chunks: list[StreamingChunk] = []
455455

456-
# Accumulators
457-
arg_by_id: dict[str, str] = {}
458-
name_by_id: dict[str, str] = {}
459-
id_order: list[str] = []
456+
# Accumulators keyed by tool_call.index (always unique per call, even for repeated tool names)
457+
arg_by_index: dict[str, str] = {}
458+
name_by_index: dict[str, str] = {}
459+
id_by_index: dict[str, str | None] = {}
460+
index_order: list[str] = []
460461
tool_call_index: int = 0
461462

462463
# track reasoning and content blocks to correctly set start=True on the first chunk of each block
@@ -484,13 +485,14 @@ async def _handle_streaming_response_async(
484485

485486
if chunk.tool_calls:
486487
for tool_call in chunk.tool_calls:
487-
tool_call_id = tool_call.id or tool_call.tool_name or ""
488+
key = str(tool_call.index)
488489
args = tool_call.arguments or ""
489490

490-
if tool_call_id not in id_order:
491-
id_order.append(tool_call_id)
492-
name_by_id[tool_call_id] = tool_call.tool_name or ""
493-
arg_by_id[tool_call_id] = args
491+
if key not in index_order:
492+
index_order.append(key)
493+
name_by_index[key] = tool_call.tool_name or ""
494+
id_by_index[key] = tool_call.id
495+
arg_by_index[key] = args
494496

495497
if callback is not None:
496498
await callback(chunk)
@@ -505,10 +507,10 @@ async def _handle_streaming_response_async(
505507
reasoning += c.reasoning.reasoning_text if c.reasoning else ""
506508

507509
tool_calls = []
508-
for tool_call_id in id_order:
509-
arguments: str = arg_by_id.get(tool_call_id, "")
510+
for key in index_order:
511+
arguments: str = arg_by_index.get(key, "")
510512
tool_calls.append(
511-
ToolCall(id=tool_call_id, tool_name=name_by_id[tool_call_id], arguments=json.loads(arguments))
513+
ToolCall(id=id_by_index[key], tool_name=name_by_index[key], arguments=json.loads(arguments))
512514
)
513515

514516
# We can't use _convert_streaming_chunks_to_chat_message because

integrations/ollama/tests/test_chat_generator.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,6 @@ def test_convert_ollama_response_to_chatmessage_with_tools(self):
190190
assert observed.role == "assistant"
191191
assert observed.text is None
192192
assert observed.tool_call == ToolCall(
193-
id="call_0",
194193
tool_name="get_current_weather",
195194
arguments={"format": "celsius", "location": "Paris, FR"},
196195
)
@@ -219,8 +218,8 @@ def test_convert_ollama_response_to_chatmessage_with_repeated_tool(self):
219218
observed = _convert_ollama_response_to_chatmessage(ollama_response)
220219

221220
assert len(observed.tool_calls) == 2
222-
assert observed.tool_calls[0] == ToolCall(id="call_0", tool_name="weather", arguments={"city": "Paris"})
223-
assert observed.tool_calls[1] == ToolCall(id="call_1", tool_name="weather", arguments={"city": "London"})
221+
assert observed.tool_calls[0] == ToolCall(tool_name="weather", arguments={"city": "Paris"})
222+
assert observed.tool_calls[1] == ToolCall(tool_name="weather", arguments={"city": "London"})
224223

225224
def test_build_chunk(self):
226225
generator = OllamaChatGenerator()
@@ -414,10 +413,10 @@ def test_callback(chunk: StreamingChunk):
414413
assert result["replies"][0].text is None
415414
assert result["replies"][0].tool_calls[0].tool_name == "calculator"
416415
assert result["replies"][0].tool_calls[0].arguments == {"expression": "7 * (4 + 2)"}
417-
assert result["replies"][0].tool_calls[0].id == "call_1"
416+
assert result["replies"][0].tool_calls[0].id is None
418417
assert result["replies"][0].tool_calls[1].tool_name == "factorial"
419418
assert result["replies"][0].tool_calls[1].arguments == {"n": 5}
420-
assert result["replies"][0].tool_calls[1].id == "call_2"
419+
assert result["replies"][0].tool_calls[1].id is None
421420
assert result["replies"][0].meta["finish_reason"] == "stop"
422421
assert result["replies"][0].meta["model"] == "qwen3:0.6b"
423422

@@ -430,7 +429,7 @@ def test_callback(chunk: StreamingChunk):
430429
expected = {
431430
"index": 1,
432431
"arguments": '{"expression": "7 * (4 + 2)"}',
433-
"id": "call_1",
432+
"id": None,
434433
"tool_name": "calculator",
435434
}
436435
# We add extra to the expected dict if it exists in the result for comparison
@@ -443,7 +442,7 @@ def test_callback(chunk: StreamingChunk):
443442
"index": 2,
444443
"tool_name": "factorial",
445444
"arguments": '{"n": 5}',
446-
"id": "call_2",
445+
"id": None,
447446
}
448447
# We add extra to the expected dict if it exists in the result for comparison
449448
# This was added in PR https://github.com/deepset-ai/haystack/pull/10018 and released in Haystack 2.20.0
@@ -503,10 +502,10 @@ def test_handle_streaming_response_repeated_tool_calls(self):
503502
assert len(result["replies"][0].tool_calls) == 2
504503
assert result["replies"][0].tool_calls[0].tool_name == "weather"
505504
assert result["replies"][0].tool_calls[0].arguments == {"city": "Paris"}
506-
assert result["replies"][0].tool_calls[0].id == "call_1"
505+
assert result["replies"][0].tool_calls[0].id is None
507506
assert result["replies"][0].tool_calls[1].tool_name == "weather"
508507
assert result["replies"][0].tool_calls[1].arguments == {"city": "London"}
509-
assert result["replies"][0].tool_calls[1].id == "call_2"
508+
assert result["replies"][0].tool_calls[1].id is None
510509

511510
def test_handle_streaming_response_tool_calls_with_thinking(self):
512511
ollama_chunks = [
@@ -622,7 +621,7 @@ def test_callback(chunk: StreamingChunk):
622621
assert result["replies"][0].text is None
623622
assert result["replies"][0].tool_calls[0].tool_name == "add_two_numbers"
624623
assert result["replies"][0].tool_calls[0].arguments == {"a": 2, "b": 2}
625-
assert result["replies"][0].tool_calls[0].id == "call_1"
624+
assert result["replies"][0].tool_calls[0].id is None
626625
assert result["replies"][0].reasoning.reasoning_text == "Okay, the user is asking 2 plus 2."
627626
assert result["replies"][0].meta["finish_reason"] == "stop"
628627
assert result["replies"][0].meta["model"] == "qwen3:0.6b"
@@ -651,7 +650,7 @@ def test_callback(chunk: StreamingChunk):
651650
expected = {
652651
"index": 1,
653652
"arguments": '{"a": 2, "b": 2}',
654-
"id": "call_1",
653+
"id": None,
655654
"tool_name": "add_two_numbers",
656655
}
657656
serialized_dict = streaming_chunks[12].tool_calls[0].to_dict()

0 commit comments

Comments
 (0)