main
py 191 lines 6.35 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # ruff: noqa: E501
4
5 import json
6 from unittest.mock import MagicMock
7
8 import pytest
9
10 from tests.tool_parsers.utils import (
11 run_tool_extraction,
12 run_tool_extraction_streaming,
13 )
14 from vllm.entrypoints.generate.base.protocol import FunctionCall, ToolCall
15 from vllm.tool_parsers import ToolParser, ToolParserManager
16
17
18 def make_tool_call(name, arguments):
19 return ToolCall(
20 type="function",
21 function=FunctionCall(name=name, arguments=json.dumps(arguments)),
22 )
23
24
25 # TODO: add reason prefix and suffix.
26
27
28 @pytest.mark.parametrize(
29 "model_output,expected_tool_calls,expected_content",
30 [
31 # No tool call
32 ("How can I help you today?", [], "How can I help you today?"),
33 # Single tool call, no content
34 (
35 '<tool_calls>[{"name": "get_weather", "arguments": {"city": "San Francisco", "metric": "celsius"}}]</tool_calls>', # noqa: E501
36 [
37 make_tool_call(
38 "get_weather", {"city": "San Francisco", "metric": "celsius"}
39 )
40 ],
41 None,
42 ),
43 # Multiple tool calls
44 (
45 '<tool_calls>[{"name": "get_weather", "arguments": {"city": "San Francisco", "metric": "celsius"}}, {"name": "register_user", "arguments": {"name": "John Doe", "age": 37, "address": {"city": "San Francisco", "state": "CA"}, "role": null, "passed_test": true, "aliases": ["John", "Johnny"]}}]</tool_calls>', # noqa: E501
46 [
47 make_tool_call(
48 "get_weather", {"city": "San Francisco", "metric": "celsius"}
49 ),
50 make_tool_call(
51 "register_user",
52 {
53 "name": "John Doe",
54 "age": 37,
55 "address": {"city": "San Francisco", "state": "CA"},
56 "role": None,
57 "passed_test": True,
58 "aliases": ["John", "Johnny"],
59 },
60 ),
61 ],
62 None,
63 ),
64 # Content before tool call
65 (
66 'I will call the tool now. <tool_calls>[{"name": "get_weather", "arguments": {"city": "Boston"}}]</tool_calls>', # noqa: E501
67 [make_tool_call("get_weather", {"city": "Boston"})],
68 "I will call the tool now. ",
69 ),
70 # Content after tool call (should be stripped)
71 (
72 '<tool_calls>[{"name": "get_weather", "arguments": {"city": "Seattle"}}]</tool_calls>\nThank you!', # noqa: E501
73 [make_tool_call("get_weather", {"city": "Seattle"})],
74 None,
75 ),
76 (
77 '<tool_calls>[{"name": "complex_tool", "arguments": {"level1": {"level2": {"level3": {"value": 123}}}}}]</tool_calls>',
78 [
79 make_tool_call(
80 "complex_tool", {"level1": {"level2": {"level3": {"value": 123}}}}
81 )
82 ],
83 None,
84 ),
85 ],
86 )
87 def test_hunyuan_a13b_tool_parser_extract(
88 model_output, expected_tool_calls, expected_content
89 ):
90 mock_tokenizer = MagicMock()
91 tool_parser: ToolParser = ToolParserManager.get_tool_parser("hunyuan_a13b")(
92 mock_tokenizer
93 )
94 content, tool_calls = run_tool_extraction(
95 tool_parser, model_output, streaming=False
96 )
97
98 # align the random id.
99 for idx in range(len(tool_calls)):
100 tool_calls[idx].id = expected_tool_calls[idx].id
101 assert tool_calls == expected_tool_calls
102 assert content == expected_content
103
104
105 # Streaming test: simulate incremental output
106 @pytest.mark.parametrize(
107 "model_deltas,expected_tool_calls",
108 [
109 (
110 [
111 '<tool_calls>[{"name": "get_weather", ',
112 '"arguments": {"city": "San Francisco", ',
113 '"metric": "celsius"}}]',
114 "</tool_calls>",
115 ],
116 [
117 make_tool_call(
118 "get_weather", {"city": "San Francisco", "metric": "celsius"}
119 )
120 ],
121 ),
122 (
123 [
124 '<tool_calls>[{"name":',
125 ' "get_weather",',
126 ' "arguments":',
127 ' {"city": "Boston"}',
128 "}]",
129 "</tool_calls>",
130 ],
131 [make_tool_call("get_weather", {"city": "Boston"})],
132 ),
133 (
134 [
135 "",
136 '<tool_calls>[{"name":',
137 ' "get_weather",',
138 ' "arguments":',
139 ' {"city": "Boston"}',
140 "}]",
141 "</tool_calls>",
142 "\n</answer>",
143 ],
144 [make_tool_call("get_weather", {"city": "Boston"})],
145 ),
146 pytest.param(
147 [
148 '<tool_calls>[{"name": "complex_tool",',
149 ' "arguments": ',
150 ' {"level1": {"level2": ',
151 '{"level3": {"value": 123}}}}}',
152 "]</tool_calls>",
153 ],
154 [
155 make_tool_call(
156 "complex_tool", {"level1": {"level2": {"level3": {"value": 123}}}}
157 )
158 ],
159 marks=pytest.mark.xfail(
160 reason="stream parsing not support nested json yet."
161 ),
162 ),
163 ],
164 )
165 def test_hunyuan_a13b_tool_parser_streaming(model_deltas, expected_tool_calls):
166 mock_tokenizer = MagicMock()
167
168 tool_parser: ToolParser = ToolParserManager.get_tool_parser("hunyuan_a13b")(
169 mock_tokenizer
170 )
171 reconstructor = run_tool_extraction_streaming(
172 tool_parser, model_deltas, assert_one_tool_per_delta=False
173 )
174
175 # align the random id.
176 for idx in range(len(reconstructor.tool_calls)):
177 reconstructor.tool_calls[idx].id = expected_tool_calls[idx].id
178
179 assert reconstructor.tool_calls == expected_tool_calls
180
181
182 def test_hunyuan_a13b_tool_parser_non_ascii():
183 mock_tokenizer = MagicMock()
184 tool_parser: ToolParser = ToolParserManager.get_tool_parser("hunyuan_a13b")(
185 mock_tokenizer
186 )
187 model_output = '<tool_calls>[{"name": "get_weather", "arguments": {"city": "北京"}}]</tool_calls>'
188 _, tool_calls = run_tool_extraction(tool_parser, model_output, streaming=False)
189 args = tool_calls[0].function.arguments
190 assert "北京" in args
191 assert "\\u" not in args