1+ from litellm .utils import ChatCompletionDeltaToolCall , Function
2+ import json
3+
4+ from .toolcall_types import ResolvedToolCall , ResolvedFunction
5+
6+ class ToolCallList ():
7+ """
8+ A helper object that defines a custom `__iadd__()` method which accepts a
9+ `tool_call_deltas: list[ChatCompletionDeltaToolCall]` argument. This class
10+ is used to aggregate the tool call deltas yielded from a LiteLLM response
11+ stream and produce a list of tool calls.
12+
13+ After all tool call deltas are added, the `process()` method may be called
14+ to return a list of resolved tool calls.
15+
16+ Example usage:
17+
18+ ```py
19+ tool_call_list = ToolCallList()
20+ reply_stream = await litellm.acompletion(..., stream=True)
21+
22+ async for chunk in reply_stream:
23+ tool_call_delta = chunk.choices[0].delta.tool_calls
24+ tool_call_list += tool_call_delta
25+
26+ tool_call_list.resolve()
27+ ```
28+ """
29+
30+ _aggregate : list [ChatCompletionDeltaToolCall ]
31+
32+ def __init__ (self ):
33+ self .size = None
34+
35+ # Initialize `_aggregate`
36+ self ._aggregate = []
37+
38+
39+ def __iadd__ (self , other : list [ChatCompletionDeltaToolCall ] | None ) -> 'ToolCallList' :
40+ """
41+ Adds a list of tool call deltas to this instance.
42+
43+ NOTE: This assumes the 'index' attribute on each entry in this list to
44+ be accurate. If this assumption doesn't hold, we will need to rework the
45+ logic here.
46+ """
47+ if other is None :
48+ return self
49+
50+ # Iterate through each delta
51+ for delta in other :
52+ # Ensure `self._aggregate` is at least of size `delta.index + 1`
53+ for i in range (len (self ._aggregate ), delta .index + 1 ):
54+ self ._aggregate .append (ChatCompletionDeltaToolCall (
55+ function = Function (arguments = "" ),
56+ index = i ,
57+ ))
58+
59+ # Find the corresponding target in the `self._aggregate` and add the
60+ # delta on top of it. In most cases, the value of aggregate
61+ # attribute is set as soon as any delta sets it to a non-`None`
62+ # value. However, `delta.function.arguments` is a string that should
63+ # be appended to the aggregate value of that attribute.
64+ target = self ._aggregate [delta .index ]
65+ if delta .type :
66+ target .type = delta .type
67+ if delta .id :
68+ target .id = delta .id
69+ if delta .function .name :
70+ target .function .name = delta .function .name
71+ if delta .function .arguments :
72+ target .function .arguments += delta .function .arguments
73+
74+ return self
75+
76+
77+ def __add__ (self , other : list [ChatCompletionDeltaToolCall ] | None ) -> 'ToolCallList' :
78+ """
79+ Alias for `__iadd__()`.
80+ """
81+ return self .__iadd__ (other )
82+
83+
84+ def resolve (self ) -> list [ResolvedToolCall ]:
85+ """
86+ Resolve the aggregated tool call delta lists into a list of tool calls.
87+ """
88+ resolved_toolcalls : list [ResolvedToolCall ] = []
89+ for i , raw_toolcall in enumerate (self ._aggregate ):
90+ # Verify entries are at the correct index in the aggregated list
91+ assert raw_toolcall .index == i
92+
93+ # Verify each tool call specifies the name of the tool to run.
94+ #
95+ # TODO: Check if this may cause a runtime error. The docstring on
96+ # `litellm.utils.Function` implies that `name` may be `None`.
97+ assert raw_toolcall .function .name
98+
99+ # Verify each tool call defines the type of tool it is calling.
100+ assert raw_toolcall .type is not None
101+
102+ # Parse the function argument string into a dictionary
103+ resolved_fn_args = json .loads (raw_toolcall .function .arguments )
104+
105+ # Add to the returned list
106+ resolved_fn = ResolvedFunction (
107+ name = raw_toolcall .function .name ,
108+ arguments = resolved_fn_args
109+ )
110+ resolved_toolcall = ResolvedToolCall (
111+ id = raw_toolcall .id ,
112+ type = raw_toolcall .type ,
113+ index = i ,
114+ function = resolved_fn
115+ )
116+ resolved_toolcalls .append (resolved_toolcall )
117+
118+ return resolved_toolcalls
119+
120+
121+
0 commit comments