@@ -194,6 +194,33 @@ def test_convert_ollama_response_to_chatmessage_with_tools(self):
194194 arguments = {"format" : "celsius" , "location" : "Paris, FR" },
195195 )
196196
197+ def test_convert_ollama_response_to_chatmessage_with_repeated_tool (self ):
198+ ollama_response = ChatResponse (
199+ model = "some_model" ,
200+ created_at = "2023-12-12T14:13:43.416799Z" ,
201+ message = {
202+ "role" : "assistant" ,
203+ "content" : "" ,
204+ "tool_calls" : [
205+ {"function" : {"name" : "weather" , "arguments" : {"city" : "Paris" }}},
206+ {"function" : {"name" : "weather" , "arguments" : {"city" : "London" }}},
207+ ],
208+ },
209+ done = True ,
210+ total_duration = 5191566416 ,
211+ load_duration = 2154458 ,
212+ prompt_eval_count = 26 ,
213+ prompt_eval_duration = 383809000 ,
214+ eval_count = 298 ,
215+ eval_duration = 4799921000 ,
216+ )
217+
218+ observed = _convert_ollama_response_to_chatmessage (ollama_response )
219+
220+ assert len (observed .tool_calls ) == 2
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" })
223+
197224 def test_build_chunk (self ):
198225 generator = OllamaChatGenerator ()
199226
@@ -386,8 +413,10 @@ def test_callback(chunk: StreamingChunk):
386413 assert result ["replies" ][0 ].text is None
387414 assert result ["replies" ][0 ].tool_calls [0 ].tool_name == "calculator"
388415 assert result ["replies" ][0 ].tool_calls [0 ].arguments == {"expression" : "7 * (4 + 2)" }
416+ assert result ["replies" ][0 ].tool_calls [0 ].id is None
389417 assert result ["replies" ][0 ].tool_calls [1 ].tool_name == "factorial"
390418 assert result ["replies" ][0 ].tool_calls [1 ].arguments == {"n" : 5 }
419+ assert result ["replies" ][0 ].tool_calls [1 ].id is None
391420 assert result ["replies" ][0 ].meta ["finish_reason" ] == "stop"
392421 assert result ["replies" ][0 ].meta ["model" ] == "qwen3:0.6b"
393422
@@ -422,6 +451,123 @@ def test_callback(chunk: StreamingChunk):
422451 assert streaming_chunks [1 ].tool_calls [0 ].to_dict () == expected
423452 assert len (streaming_chunks [2 ].tool_calls ) == 0
424453
454+ def test_handle_streaming_response_repeated_tool_calls (self ):
455+ ollama_chunks = [
456+ ChatResponse (
457+ model = "qwen3:0.6b" ,
458+ created_at = "2025-07-31T14:48:03.471292Z" ,
459+ done = False ,
460+ message = Message (
461+ role = "assistant" ,
462+ content = "" ,
463+ tool_calls = [
464+ Message .ToolCall (
465+ function = Message .ToolCall .Function (name = "weather" , arguments = {"city" : "Paris" })
466+ )
467+ ],
468+ ),
469+ ),
470+ ChatResponse (
471+ model = "qwen3:0.6b" ,
472+ created_at = "2025-07-31T14:48:03.660179Z" ,
473+ done = False ,
474+ message = Message (
475+ role = "assistant" ,
476+ content = "" ,
477+ tool_calls = [
478+ Message .ToolCall (
479+ function = Message .ToolCall .Function (name = "weather" , arguments = {"city" : "London" })
480+ )
481+ ],
482+ ),
483+ ),
484+ ChatResponse (
485+ model = "qwen3:0.6b" ,
486+ created_at = "2025-07-31T14:48:03.678729Z" ,
487+ done = True ,
488+ done_reason = "stop" ,
489+ total_duration = 774786292 ,
490+ load_duration = 43608375 ,
491+ prompt_eval_count = 217 ,
492+ prompt_eval_duration = 312974541 ,
493+ eval_count = 46 ,
494+ eval_duration = 417069750 ,
495+ message = Message (role = "assistant" , content = "" ),
496+ ),
497+ ]
498+
499+ generator = OllamaChatGenerator ()
500+ result = generator ._handle_streaming_response (ollama_chunks , None )
501+
502+ assert len (result ["replies" ][0 ].tool_calls ) == 2
503+ assert result ["replies" ][0 ].tool_calls [0 ].tool_name == "weather"
504+ assert result ["replies" ][0 ].tool_calls [0 ].arguments == {"city" : "Paris" }
505+ assert result ["replies" ][0 ].tool_calls [0 ].id is None
506+ assert result ["replies" ][0 ].tool_calls [1 ].tool_name == "weather"
507+ assert result ["replies" ][0 ].tool_calls [1 ].arguments == {"city" : "London" }
508+ assert result ["replies" ][0 ].tool_calls [1 ].id is None
509+
510+ @pytest .mark .asyncio
511+ async def test_handle_streaming_response_async_repeated_tool_calls (self ):
512+ ollama_chunks = [
513+ ChatResponse (
514+ model = "qwen3:0.6b" ,
515+ created_at = "2025-07-31T14:48:03.471292Z" ,
516+ done = False ,
517+ message = Message (
518+ role = "assistant" ,
519+ content = "" ,
520+ tool_calls = [
521+ Message .ToolCall (
522+ function = Message .ToolCall .Function (name = "weather" , arguments = {"city" : "Paris" })
523+ )
524+ ],
525+ ),
526+ ),
527+ ChatResponse (
528+ model = "qwen3:0.6b" ,
529+ created_at = "2025-07-31T14:48:03.660179Z" ,
530+ done = False ,
531+ message = Message (
532+ role = "assistant" ,
533+ content = "" ,
534+ tool_calls = [
535+ Message .ToolCall (
536+ function = Message .ToolCall .Function (name = "weather" , arguments = {"city" : "London" })
537+ )
538+ ],
539+ ),
540+ ),
541+ ChatResponse (
542+ model = "qwen3:0.6b" ,
543+ created_at = "2025-07-31T14:48:03.678729Z" ,
544+ done = True ,
545+ done_reason = "stop" ,
546+ total_duration = 774786292 ,
547+ load_duration = 43608375 ,
548+ prompt_eval_count = 217 ,
549+ prompt_eval_duration = 312974541 ,
550+ eval_count = 46 ,
551+ eval_duration = 417069750 ,
552+ message = Message (role = "assistant" , content = "" ),
553+ ),
554+ ]
555+
556+ async def async_chunks ():
557+ for chunk in ollama_chunks :
558+ yield chunk
559+
560+ generator = OllamaChatGenerator ()
561+ result = await generator ._handle_streaming_response_async (async_chunks (), None )
562+
563+ assert len (result ["replies" ][0 ].tool_calls ) == 2
564+ assert result ["replies" ][0 ].tool_calls [0 ].tool_name == "weather"
565+ assert result ["replies" ][0 ].tool_calls [0 ].arguments == {"city" : "Paris" }
566+ assert result ["replies" ][0 ].tool_calls [0 ].id is None
567+ assert result ["replies" ][0 ].tool_calls [1 ].tool_name == "weather"
568+ assert result ["replies" ][0 ].tool_calls [1 ].arguments == {"city" : "London" }
569+ assert result ["replies" ][0 ].tool_calls [1 ].id is None
570+
425571 def test_handle_streaming_response_tool_calls_with_thinking (self ):
426572 ollama_chunks = [
427573 ChatResponse (
@@ -536,6 +682,7 @@ def test_callback(chunk: StreamingChunk):
536682 assert result ["replies" ][0 ].text is None
537683 assert result ["replies" ][0 ].tool_calls [0 ].tool_name == "add_two_numbers"
538684 assert result ["replies" ][0 ].tool_calls [0 ].arguments == {"a" : 2 , "b" : 2 }
685+ assert result ["replies" ][0 ].tool_calls [0 ].id is None
539686 assert result ["replies" ][0 ].reasoning .reasoning_text == "Okay, the user is asking 2 plus 2."
540687 assert result ["replies" ][0 ].meta ["finish_reason" ] == "stop"
541688 assert result ["replies" ][0 ].meta ["model" ] == "qwen3:0.6b"
@@ -1306,6 +1453,33 @@ def multiply(a: int, b: int) -> int:
13061453 assert new_response .tool_calls [0 ].tool_name == "multiply"
13071454 assert new_response .tool_calls [0 ].arguments == {"a" : 5 , "b" : 10 }
13081455
1456+ @pytest .mark .parametrize ("streaming_callback" , [None , print_streaming_chunk ])
1457+ def test_live_run_with_repeated_tool_calls (self , tools , streaming_callback ):
1458+ component = OllamaChatGenerator (model = "qwen3:0.6b" , tools = tools , streaming_callback = streaming_callback )
1459+ tool_invoker = ToolInvoker (tools = tools )
1460+
1461+ messages = [ChatMessage .from_user ("What is the weather in Paris and London?" )]
1462+ response = component .run (messages )
1463+
1464+ assert len (response ["replies" ]) == 1
1465+ assistant_msg = response ["replies" ][0 ]
1466+
1467+ assert assistant_msg .tool_calls
1468+ assert len (assistant_msg .tool_calls ) == 2
1469+ for tc in assistant_msg .tool_calls :
1470+ assert isinstance (tc , ToolCall )
1471+ assert tc .tool_name == "weather"
1472+ assert "city" in tc .arguments
1473+
1474+ cities = {tc .arguments ["city" ].lower () for tc in assistant_msg .tool_calls }
1475+ assert any ("paris" in c for c in cities )
1476+ assert any ("london" in c for c in cities )
1477+
1478+ tool_messages = tool_invoker .run (messages = [assistant_msg ])["tool_messages" ]
1479+ final_response = component .run ([* messages , assistant_msg , * tool_messages ])
1480+ assert len (final_response ["replies" ]) == 1
1481+ assert final_response ["replies" ][0 ].text
1482+
13091483 def test_live_run_with_tools_and_format (self , tools ):
13101484 response_format = {
13111485 "type" : "object" ,
0 commit comments