Convert formatting to use ruff instead of yapf + isort (#26247)
Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
This commit is contained in:
@@ -7,14 +7,17 @@ from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from vllm.entrypoints.openai.protocol import (ChatCompletionRequest,
|
||||
ChatCompletionToolsParam,
|
||||
DeltaMessage, FunctionCall,
|
||||
ToolCall)
|
||||
from vllm.entrypoints.openai.protocol import (
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionToolsParam,
|
||||
DeltaMessage,
|
||||
FunctionCall,
|
||||
ToolCall,
|
||||
)
|
||||
from vllm.entrypoints.openai.tool_parsers.qwen3coder_tool_parser import (
|
||||
Qwen3CoderToolParser)
|
||||
from vllm.entrypoints.openai.tool_parsers.qwen3xml_tool_parser import (
|
||||
Qwen3XMLToolParser)
|
||||
Qwen3CoderToolParser,
|
||||
)
|
||||
from vllm.entrypoints.openai.tool_parsers.qwen3xml_tool_parser import Qwen3XMLToolParser
|
||||
from vllm.transformers_utils.detokenizer_utils import detokenize_incrementally
|
||||
from vllm.transformers_utils.tokenizer import AnyTokenizer, get_tokenizer
|
||||
|
||||
@@ -39,8 +42,7 @@ def qwen3_xml_tool_parser(qwen3_tokenizer):
|
||||
|
||||
|
||||
@pytest.fixture(params=["original", "xml"])
|
||||
def qwen3_tool_parser_parametrized(qwen3_tool_parser, qwen3_xml_tool_parser,
|
||||
request):
|
||||
def qwen3_tool_parser_parametrized(qwen3_tool_parser, qwen3_xml_tool_parser, request):
|
||||
"""Parameterized fixture that provides both parser types for testing"""
|
||||
if request.param == "original":
|
||||
return qwen3_tool_parser
|
||||
@@ -51,76 +53,63 @@ def qwen3_tool_parser_parametrized(qwen3_tool_parser, qwen3_xml_tool_parser,
|
||||
@pytest.fixture
|
||||
def sample_tools():
|
||||
return [
|
||||
ChatCompletionToolsParam(type="function",
|
||||
function={
|
||||
"name": "get_current_weather",
|
||||
"description": "Get the current weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {
|
||||
"type": "string",
|
||||
"description": "The city name"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description":
|
||||
"The state code"
|
||||
},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum":
|
||||
["fahrenheit", "celsius"]
|
||||
}
|
||||
},
|
||||
"required": ["city", "state"]
|
||||
}
|
||||
}),
|
||||
ChatCompletionToolsParam(type="function",
|
||||
function={
|
||||
"name": "calculate_area",
|
||||
"description":
|
||||
"Calculate area of a shape",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shape": {
|
||||
"type": "string"
|
||||
},
|
||||
"dimensions": {
|
||||
"type": "object"
|
||||
},
|
||||
"precision": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
ChatCompletionToolsParam(
|
||||
type="function",
|
||||
function={
|
||||
"name": "get_current_weather",
|
||||
"description": "Get the current weather",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"city": {"type": "string", "description": "The city name"},
|
||||
"state": {"type": "string", "description": "The state code"},
|
||||
"unit": {"type": "string", "enum": ["fahrenheit", "celsius"]},
|
||||
},
|
||||
"required": ["city", "state"],
|
||||
},
|
||||
},
|
||||
),
|
||||
ChatCompletionToolsParam(
|
||||
type="function",
|
||||
function={
|
||||
"name": "calculate_area",
|
||||
"description": "Calculate area of a shape",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"shape": {"type": "string"},
|
||||
"dimensions": {"type": "object"},
|
||||
"precision": {"type": "integer"},
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def assert_tool_calls(actual_tool_calls: list[ToolCall],
|
||||
expected_tool_calls: list[ToolCall]):
|
||||
def assert_tool_calls(
|
||||
actual_tool_calls: list[ToolCall], expected_tool_calls: list[ToolCall]
|
||||
):
|
||||
assert len(actual_tool_calls) == len(expected_tool_calls)
|
||||
|
||||
for actual_tool_call, expected_tool_call in zip(actual_tool_calls,
|
||||
expected_tool_calls):
|
||||
for actual_tool_call, expected_tool_call in zip(
|
||||
actual_tool_calls, expected_tool_calls
|
||||
):
|
||||
# Qwen3 parser doesn't generate IDs during extraction
|
||||
assert actual_tool_call.type == "function"
|
||||
assert (
|
||||
actual_tool_call.function.name == expected_tool_call.function.name)
|
||||
assert (json.loads(actual_tool_call.function.arguments) == json.loads(
|
||||
expected_tool_call.function.arguments))
|
||||
assert actual_tool_call.function.name == expected_tool_call.function.name
|
||||
assert json.loads(actual_tool_call.function.arguments) == json.loads(
|
||||
expected_tool_call.function.arguments
|
||||
)
|
||||
|
||||
|
||||
def stream_delta_message_generator(
|
||||
qwen3_tool_parser,
|
||||
qwen3_tokenizer: AnyTokenizer,
|
||||
model_output: str,
|
||||
request: Optional[ChatCompletionRequest] = None
|
||||
request: Optional[ChatCompletionRequest] = None,
|
||||
) -> Generator[DeltaMessage, None, None]:
|
||||
all_token_ids = qwen3_tokenizer.encode(model_output,
|
||||
add_special_tokens=False)
|
||||
all_token_ids = qwen3_tokenizer.encode(model_output, add_special_tokens=False)
|
||||
|
||||
previous_text = ""
|
||||
previous_tokens = None
|
||||
@@ -129,18 +118,19 @@ def stream_delta_message_generator(
|
||||
for i, delta_token in enumerate(all_token_ids):
|
||||
delta_token_ids = [delta_token]
|
||||
previous_token_ids = all_token_ids[:i]
|
||||
current_token_ids = all_token_ids[:i + 1]
|
||||
current_token_ids = all_token_ids[: i + 1]
|
||||
|
||||
(new_tokens, delta_text, new_prefix_offset,
|
||||
new_read_offset) = detokenize_incrementally(
|
||||
tokenizer=qwen3_tokenizer,
|
||||
all_input_ids=current_token_ids,
|
||||
prev_tokens=previous_tokens,
|
||||
prefix_offset=prefix_offset,
|
||||
read_offset=read_offset,
|
||||
skip_special_tokens=False,
|
||||
spaces_between_special_tokens=True,
|
||||
)
|
||||
(new_tokens, delta_text, new_prefix_offset, new_read_offset) = (
|
||||
detokenize_incrementally(
|
||||
tokenizer=qwen3_tokenizer,
|
||||
all_input_ids=current_token_ids,
|
||||
prev_tokens=previous_tokens,
|
||||
prefix_offset=prefix_offset,
|
||||
read_offset=read_offset,
|
||||
skip_special_tokens=False,
|
||||
spaces_between_special_tokens=True,
|
||||
)
|
||||
)
|
||||
|
||||
current_text = previous_text + delta_text
|
||||
|
||||
@@ -157,8 +147,9 @@ def stream_delta_message_generator(
|
||||
yield delta_message
|
||||
|
||||
previous_text = current_text
|
||||
previous_tokens = (previous_tokens +
|
||||
new_tokens if previous_tokens else new_tokens)
|
||||
previous_tokens = (
|
||||
previous_tokens + new_tokens if previous_tokens else new_tokens
|
||||
)
|
||||
prefix_offset = new_prefix_offset
|
||||
read_offset = new_read_offset
|
||||
|
||||
@@ -166,7 +157,8 @@ def stream_delta_message_generator(
|
||||
def test_extract_tool_calls_no_tools(qwen3_tool_parser_parametrized):
|
||||
model_output = "This is a test response without any tool calls"
|
||||
extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
|
||||
model_output, request=None) # type: ignore[arg-type]
|
||||
model_output, request=None
|
||||
) # type: ignore[arg-type]
|
||||
assert not extracted_tool_calls.tools_called
|
||||
assert extracted_tool_calls.tool_calls == []
|
||||
assert extracted_tool_calls.content == model_output
|
||||
@@ -182,7 +174,8 @@ def test_extract_tool_calls_no_tools(qwen3_tool_parser_parametrized):
|
||||
],
|
||||
argnames=["model_output", "expected_tool_calls", "expected_content"],
|
||||
argvalues=[
|
||||
('''<tool_call>
|
||||
(
|
||||
"""<tool_call>
|
||||
<function=get_current_weather>
|
||||
<parameter=city>
|
||||
Dallas
|
||||
@@ -194,16 +187,21 @@ TX
|
||||
fahrenheit
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(
|
||||
function=FunctionCall(name="get_current_weather",
|
||||
arguments=json.dumps({
|
||||
"city": "Dallas",
|
||||
"state": "TX",
|
||||
"unit": "fahrenheit"
|
||||
})))
|
||||
], None),
|
||||
('''Sure! Let me check the weather for you.<tool_call>
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="get_current_weather",
|
||||
arguments=json.dumps(
|
||||
{"city": "Dallas", "state": "TX", "unit": "fahrenheit"}
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
None,
|
||||
),
|
||||
(
|
||||
"""Sure! Let me check the weather for you.<tool_call>
|
||||
<function=get_current_weather>
|
||||
<parameter=city>
|
||||
Dallas
|
||||
@@ -215,16 +213,21 @@ TX
|
||||
fahrenheit
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(
|
||||
function=FunctionCall(name="get_current_weather",
|
||||
arguments=json.dumps({
|
||||
"city": "Dallas",
|
||||
"state": "TX",
|
||||
"unit": "fahrenheit"
|
||||
})))
|
||||
], "Sure! Let me check the weather for you."),
|
||||
('''<tool_call>
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="get_current_weather",
|
||||
arguments=json.dumps(
|
||||
{"city": "Dallas", "state": "TX", "unit": "fahrenheit"}
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
"Sure! Let me check the weather for you.",
|
||||
),
|
||||
(
|
||||
"""<tool_call>
|
||||
<function=calculate_area>
|
||||
<parameter=shape>
|
||||
rectangle
|
||||
@@ -237,18 +240,25 @@ rectangle
|
||||
2
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(function=FunctionCall(name="calculate_area",
|
||||
arguments=json.dumps({
|
||||
"shape": "rectangle",
|
||||
"dimensions": {
|
||||
"width": 10,
|
||||
"height": 20
|
||||
},
|
||||
"precision": 2
|
||||
})))
|
||||
], None),
|
||||
('''<tool_call>
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="calculate_area",
|
||||
arguments=json.dumps(
|
||||
{
|
||||
"shape": "rectangle",
|
||||
"dimensions": {"width": 10, "height": 20},
|
||||
"precision": 2,
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
None,
|
||||
),
|
||||
(
|
||||
"""<tool_call>
|
||||
<function=get_current_weather>
|
||||
<parameter=city>
|
||||
Dallas
|
||||
@@ -273,23 +283,29 @@ FL
|
||||
fahrenheit
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(
|
||||
function=FunctionCall(name="get_current_weather",
|
||||
arguments=json.dumps({
|
||||
"city": "Dallas",
|
||||
"state": "TX",
|
||||
"unit": "fahrenheit"
|
||||
}))),
|
||||
ToolCall(
|
||||
function=FunctionCall(name="get_current_weather",
|
||||
arguments=json.dumps({
|
||||
"city": "Orlando",
|
||||
"state": "FL",
|
||||
"unit": "fahrenheit"
|
||||
})))
|
||||
], None),
|
||||
('''Let me calculate that area for you.<tool_call>
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="get_current_weather",
|
||||
arguments=json.dumps(
|
||||
{"city": "Dallas", "state": "TX", "unit": "fahrenheit"}
|
||||
),
|
||||
)
|
||||
),
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="get_current_weather",
|
||||
arguments=json.dumps(
|
||||
{"city": "Orlando", "state": "FL", "unit": "fahrenheit"}
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
None,
|
||||
),
|
||||
(
|
||||
"""Let me calculate that area for you.<tool_call>
|
||||
<function=calculate_area>
|
||||
<parameter=shape>
|
||||
circle
|
||||
@@ -301,26 +317,36 @@ circle
|
||||
3
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(function=FunctionCall(name="calculate_area",
|
||||
arguments=json.dumps({
|
||||
"shape": "circle",
|
||||
"dimensions": {
|
||||
"radius": 15.5
|
||||
},
|
||||
"precision": 3
|
||||
})))
|
||||
], "Let me calculate that area for you."),
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="calculate_area",
|
||||
arguments=json.dumps(
|
||||
{
|
||||
"shape": "circle",
|
||||
"dimensions": {"radius": 15.5},
|
||||
"precision": 3,
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
"Let me calculate that area for you.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_extract_tool_calls(qwen3_tool_parser_parametrized, sample_tools,
|
||||
model_output, expected_tool_calls,
|
||||
expected_content):
|
||||
request = ChatCompletionRequest(model=MODEL,
|
||||
messages=[],
|
||||
tools=sample_tools)
|
||||
def test_extract_tool_calls(
|
||||
qwen3_tool_parser_parametrized,
|
||||
sample_tools,
|
||||
model_output,
|
||||
expected_tool_calls,
|
||||
expected_content,
|
||||
):
|
||||
request = ChatCompletionRequest(model=MODEL, messages=[], tools=sample_tools)
|
||||
extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
|
||||
model_output, request=request)
|
||||
model_output, request=request
|
||||
)
|
||||
assert extracted_tool_calls.tools_called
|
||||
|
||||
assert_tool_calls(extracted_tool_calls.tool_calls, expected_tool_calls)
|
||||
@@ -328,60 +354,51 @@ def test_extract_tool_calls(qwen3_tool_parser_parametrized, sample_tools,
|
||||
assert extracted_tool_calls.content == expected_content
|
||||
|
||||
|
||||
def test_extract_tool_calls_fallback_no_tags(qwen3_tool_parser_parametrized,
|
||||
sample_tools):
|
||||
def test_extract_tool_calls_fallback_no_tags(
|
||||
qwen3_tool_parser_parametrized, sample_tools
|
||||
):
|
||||
"""Test fallback parsing when XML tags are missing"""
|
||||
model_output = '''<function=get_current_weather>
|
||||
model_output = """<function=get_current_weather>
|
||||
<parameter=city>
|
||||
Dallas
|
||||
</parameter>
|
||||
<parameter=state>
|
||||
TX
|
||||
</parameter>
|
||||
</function>'''
|
||||
</function>"""
|
||||
|
||||
request = ChatCompletionRequest(model=MODEL,
|
||||
messages=[],
|
||||
tools=sample_tools)
|
||||
request = ChatCompletionRequest(model=MODEL, messages=[], tools=sample_tools)
|
||||
extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
|
||||
model_output, request=request)
|
||||
model_output, request=request
|
||||
)
|
||||
|
||||
assert extracted_tool_calls.tools_called
|
||||
assert len(extracted_tool_calls.tool_calls) == 1
|
||||
assert (extracted_tool_calls.tool_calls[0].function.name ==
|
||||
"get_current_weather")
|
||||
assert extracted_tool_calls.tool_calls[0].function.name == "get_current_weather"
|
||||
|
||||
|
||||
def test_extract_tool_calls_type_conversion(qwen3_tool_parser_parametrized):
|
||||
"""Test parameter type conversion based on tool schema"""
|
||||
tools = [
|
||||
ChatCompletionToolsParam(type="function",
|
||||
function={
|
||||
"name": "test_types",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"int_param": {
|
||||
"type": "integer"
|
||||
},
|
||||
"float_param": {
|
||||
"type": "float"
|
||||
},
|
||||
"bool_param": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"str_param": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj_param": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
ChatCompletionToolsParam(
|
||||
type="function",
|
||||
function={
|
||||
"name": "test_types",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"int_param": {"type": "integer"},
|
||||
"float_param": {"type": "float"},
|
||||
"bool_param": {"type": "boolean"},
|
||||
"str_param": {"type": "string"},
|
||||
"obj_param": {"type": "object"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
model_output = '''<tool_call>
|
||||
model_output = """<tool_call>
|
||||
<function=test_types>
|
||||
<parameter=int_param>
|
||||
42
|
||||
@@ -399,11 +416,12 @@ hello world
|
||||
{"key": "value"}
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>'''
|
||||
</tool_call>"""
|
||||
|
||||
request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
|
||||
extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
|
||||
model_output, request=request)
|
||||
model_output, request=request
|
||||
)
|
||||
|
||||
args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
|
||||
assert args["int_param"] == 42
|
||||
@@ -425,7 +443,8 @@ hello world
|
||||
argnames=["model_output", "expected_tool_calls", "expected_content"],
|
||||
argvalues=[
|
||||
("This is a test without tools", [], "This is a test without tools"),
|
||||
('''<tool_call>
|
||||
(
|
||||
"""<tool_call>
|
||||
<function=get_current_weather>
|
||||
<parameter=city>
|
||||
Dallas
|
||||
@@ -437,16 +456,21 @@ TX
|
||||
fahrenheit
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(
|
||||
function=FunctionCall(name="get_current_weather",
|
||||
arguments=json.dumps({
|
||||
"city": "Dallas",
|
||||
"state": "TX",
|
||||
"unit": "fahrenheit"
|
||||
})))
|
||||
], None),
|
||||
('''Sure! Let me check the weather for you.<tool_call>
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="get_current_weather",
|
||||
arguments=json.dumps(
|
||||
{"city": "Dallas", "state": "TX", "unit": "fahrenheit"}
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
None,
|
||||
),
|
||||
(
|
||||
"""Sure! Let me check the weather for you.<tool_call>
|
||||
<function=get_current_weather>
|
||||
<parameter=city>
|
||||
Dallas
|
||||
@@ -458,16 +482,21 @@ TX
|
||||
fahrenheit
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(
|
||||
function=FunctionCall(name="get_current_weather",
|
||||
arguments=json.dumps({
|
||||
"city": "Dallas",
|
||||
"state": "TX",
|
||||
"unit": "fahrenheit"
|
||||
})))
|
||||
], "Sure! Let me check the weather for you."),
|
||||
('''<tool_call>
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="get_current_weather",
|
||||
arguments=json.dumps(
|
||||
{"city": "Dallas", "state": "TX", "unit": "fahrenheit"}
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
"Sure! Let me check the weather for you.",
|
||||
),
|
||||
(
|
||||
"""<tool_call>
|
||||
<function=calculate_area>
|
||||
<parameter=shape>
|
||||
rectangle
|
||||
@@ -480,18 +509,25 @@ rectangle
|
||||
2
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(function=FunctionCall(name="calculate_area",
|
||||
arguments=json.dumps({
|
||||
"shape": "rectangle",
|
||||
"dimensions": {
|
||||
"width": 10,
|
||||
"height": 20
|
||||
},
|
||||
"precision": 2
|
||||
})))
|
||||
], None),
|
||||
('''<tool_call>
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="calculate_area",
|
||||
arguments=json.dumps(
|
||||
{
|
||||
"shape": "rectangle",
|
||||
"dimensions": {"width": 10, "height": 20},
|
||||
"precision": 2,
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
None,
|
||||
),
|
||||
(
|
||||
"""<tool_call>
|
||||
<function=get_current_weather>
|
||||
<parameter=city>
|
||||
Dallas
|
||||
@@ -516,24 +552,30 @@ FL
|
||||
celsius
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(
|
||||
function=FunctionCall(name="get_current_weather",
|
||||
arguments=json.dumps({
|
||||
"city": "Dallas",
|
||||
"state": "TX",
|
||||
"unit": "fahrenheit"
|
||||
}))),
|
||||
ToolCall(
|
||||
function=FunctionCall(name="get_current_weather",
|
||||
arguments=json.dumps({
|
||||
"city": "Orlando",
|
||||
"state": "FL",
|
||||
"unit": "celsius"
|
||||
})))
|
||||
], None),
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="get_current_weather",
|
||||
arguments=json.dumps(
|
||||
{"city": "Dallas", "state": "TX", "unit": "fahrenheit"}
|
||||
),
|
||||
)
|
||||
),
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="get_current_weather",
|
||||
arguments=json.dumps(
|
||||
{"city": "Orlando", "state": "FL", "unit": "celsius"}
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
None,
|
||||
),
|
||||
# Added tool_with_typed_params test case
|
||||
('''Let me calculate that area for you.<tool_call>
|
||||
(
|
||||
"""Let me calculate that area for you.<tool_call>
|
||||
<function=calculate_area>
|
||||
<parameter=shape>
|
||||
circle
|
||||
@@ -545,33 +587,42 @@ circle
|
||||
3
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>''', [
|
||||
ToolCall(function=FunctionCall(name="calculate_area",
|
||||
arguments=json.dumps({
|
||||
"shape": "circle",
|
||||
"dimensions": {
|
||||
"radius": 15.5
|
||||
},
|
||||
"precision": 3
|
||||
})))
|
||||
], "Let me calculate that area for you."),
|
||||
</tool_call>""",
|
||||
[
|
||||
ToolCall(
|
||||
function=FunctionCall(
|
||||
name="calculate_area",
|
||||
arguments=json.dumps(
|
||||
{
|
||||
"shape": "circle",
|
||||
"dimensions": {"radius": 15.5},
|
||||
"precision": 3,
|
||||
}
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
"Let me calculate that area for you.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_extract_tool_calls_streaming(qwen3_tool_parser_parametrized,
|
||||
qwen3_tokenizer, sample_tools,
|
||||
model_output, expected_tool_calls,
|
||||
expected_content):
|
||||
def test_extract_tool_calls_streaming(
|
||||
qwen3_tool_parser_parametrized,
|
||||
qwen3_tokenizer,
|
||||
sample_tools,
|
||||
model_output,
|
||||
expected_tool_calls,
|
||||
expected_content,
|
||||
):
|
||||
"""Test incremental streaming behavior including typed parameters"""
|
||||
request = ChatCompletionRequest(model=MODEL,
|
||||
messages=[],
|
||||
tools=sample_tools)
|
||||
request = ChatCompletionRequest(model=MODEL, messages=[], tools=sample_tools)
|
||||
|
||||
other_content = ''
|
||||
other_content = ""
|
||||
tool_states = {} # Track state per tool index
|
||||
|
||||
for delta_message in stream_delta_message_generator(
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, model_output,
|
||||
request):
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, model_output, request
|
||||
):
|
||||
# role should never be streamed from tool parser
|
||||
assert not delta_message.role
|
||||
|
||||
@@ -588,7 +639,7 @@ def test_extract_tool_calls_streaming(qwen3_tool_parser_parametrized,
|
||||
"id": None,
|
||||
"name": None,
|
||||
"arguments": "",
|
||||
"type": None
|
||||
"type": None,
|
||||
}
|
||||
|
||||
# First chunk should have id, name, and type
|
||||
@@ -607,8 +658,7 @@ def test_extract_tool_calls_streaming(qwen3_tool_parser_parametrized,
|
||||
|
||||
if tool_call.function.arguments is not None:
|
||||
# Accumulate arguments incrementally
|
||||
tool_states[idx][
|
||||
"arguments"] += tool_call.function.arguments
|
||||
tool_states[idx]["arguments"] += tool_call.function.arguments
|
||||
|
||||
# Verify final content
|
||||
assert other_content == (expected_content or "") # Handle None case
|
||||
@@ -632,10 +682,11 @@ def test_extract_tool_calls_streaming(qwen3_tool_parser_parametrized,
|
||||
|
||||
|
||||
def test_extract_tool_calls_missing_closing_parameter_tag(
|
||||
qwen3_tool_parser_parametrized, sample_tools):
|
||||
qwen3_tool_parser_parametrized, sample_tools
|
||||
):
|
||||
"""Test handling of missing closing </parameter> tag"""
|
||||
# Using get_current_weather from sample_tools but with malformed XML
|
||||
model_output = '''Let me check the weather for you:
|
||||
model_output = """Let me check the weather for you:
|
||||
<tool_call>
|
||||
<function=get_current_weather>
|
||||
<parameter=city>
|
||||
@@ -647,21 +698,19 @@ TX
|
||||
fahrenheit
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>'''
|
||||
</tool_call>"""
|
||||
|
||||
request = ChatCompletionRequest(model=MODEL,
|
||||
messages=[],
|
||||
tools=sample_tools)
|
||||
request = ChatCompletionRequest(model=MODEL, messages=[], tools=sample_tools)
|
||||
extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
|
||||
model_output, request=request)
|
||||
model_output, request=request
|
||||
)
|
||||
|
||||
# The parser should handle the malformed XML gracefully
|
||||
assert extracted_tool_calls.tools_called
|
||||
assert len(extracted_tool_calls.tool_calls) == 1
|
||||
|
||||
# Verify the function name is correct
|
||||
assert extracted_tool_calls.tool_calls[
|
||||
0].function.name == "get_current_weather"
|
||||
assert extracted_tool_calls.tool_calls[0].function.name == "get_current_weather"
|
||||
|
||||
# Verify the arguments are parsed despite the missing closing tag
|
||||
args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
|
||||
@@ -675,10 +724,11 @@ fahrenheit
|
||||
|
||||
|
||||
def test_extract_tool_calls_streaming_missing_closing_tag(
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, sample_tools):
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, sample_tools
|
||||
):
|
||||
"""Test streaming with missing closing </parameter> tag"""
|
||||
# Using get_current_weather from sample_tools but with malformed XML
|
||||
model_output = '''Let me check the weather for you:
|
||||
model_output = """Let me check the weather for you:
|
||||
<tool_call>
|
||||
<function=get_current_weather>
|
||||
<parameter=city>
|
||||
@@ -690,19 +740,16 @@ TX
|
||||
fahrenheit
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>'''
|
||||
</tool_call>"""
|
||||
|
||||
request = ChatCompletionRequest(model=MODEL,
|
||||
messages=[],
|
||||
tools=sample_tools)
|
||||
request = ChatCompletionRequest(model=MODEL, messages=[], tools=sample_tools)
|
||||
|
||||
other_content = ''
|
||||
other_content = ""
|
||||
tool_states = {}
|
||||
|
||||
for delta_message in stream_delta_message_generator(
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, model_output,
|
||||
request):
|
||||
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, model_output, request
|
||||
):
|
||||
if delta_message.content:
|
||||
other_content += delta_message.content
|
||||
|
||||
@@ -715,7 +762,7 @@ fahrenheit
|
||||
"id": None,
|
||||
"name": None,
|
||||
"arguments": "",
|
||||
"type": None
|
||||
"type": None,
|
||||
}
|
||||
|
||||
if tool_call.id:
|
||||
@@ -730,8 +777,7 @@ fahrenheit
|
||||
tool_states[idx]["name"] = tool_call.function.name
|
||||
|
||||
if tool_call.function.arguments is not None:
|
||||
tool_states[idx][
|
||||
"arguments"] += tool_call.function.arguments
|
||||
tool_states[idx]["arguments"] += tool_call.function.arguments
|
||||
|
||||
# Verify content was streamed
|
||||
assert "Let me check the weather for you:" in other_content
|
||||
@@ -752,9 +798,10 @@ fahrenheit
|
||||
|
||||
|
||||
def test_extract_tool_calls_streaming_incremental(
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, sample_tools):
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, sample_tools
|
||||
):
|
||||
"""Test that streaming is truly incremental"""
|
||||
model_output = '''I'll check the weather.<tool_call>
|
||||
model_output = """I'll check the weather.<tool_call>
|
||||
<function=get_current_weather>
|
||||
<parameter=city>
|
||||
Dallas
|
||||
@@ -763,16 +810,14 @@ Dallas
|
||||
TX
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>'''
|
||||
</tool_call>"""
|
||||
|
||||
request = ChatCompletionRequest(model=MODEL,
|
||||
messages=[],
|
||||
tools=sample_tools)
|
||||
request = ChatCompletionRequest(model=MODEL, messages=[], tools=sample_tools)
|
||||
|
||||
chunks = []
|
||||
for delta_message in stream_delta_message_generator(
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, model_output,
|
||||
request):
|
||||
qwen3_tool_parser_parametrized, qwen3_tokenizer, model_output, request
|
||||
):
|
||||
chunks.append(delta_message)
|
||||
|
||||
# Should have multiple chunks
|
||||
@@ -787,7 +832,7 @@ TX
|
||||
for chunk in chunks:
|
||||
if chunk.tool_calls and chunk.tool_calls[0].id:
|
||||
header_found = True
|
||||
assert (chunk.tool_calls[0].function.name == "get_current_weather")
|
||||
assert chunk.tool_calls[0].function.name == "get_current_weather"
|
||||
assert chunk.tool_calls[0].type == "function"
|
||||
# Empty initially
|
||||
assert chunk.tool_calls[0].function.arguments == ""
|
||||
@@ -811,46 +856,40 @@ TX
|
||||
|
||||
|
||||
def test_extract_tool_calls_complex_type_with_single_quote(
|
||||
qwen3_tool_parser_parametrized):
|
||||
qwen3_tool_parser_parametrized,
|
||||
):
|
||||
"""Test parameter type conversion based on tool schema"""
|
||||
tools = [
|
||||
ChatCompletionToolsParam(type="function",
|
||||
function={
|
||||
"name": "test_types",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"int_param": {
|
||||
"type": "integer"
|
||||
},
|
||||
"float_param": {
|
||||
"type": "float"
|
||||
},
|
||||
"bool_param": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"str_param": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj_param": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
ChatCompletionToolsParam(
|
||||
type="function",
|
||||
function={
|
||||
"name": "test_types",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"int_param": {"type": "integer"},
|
||||
"float_param": {"type": "float"},
|
||||
"bool_param": {"type": "boolean"},
|
||||
"str_param": {"type": "string"},
|
||||
"obj_param": {"type": "object"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
]
|
||||
|
||||
model_output = '''<tool_call>
|
||||
model_output = """<tool_call>
|
||||
<function=test_types>
|
||||
<parameter=obj_param>
|
||||
{'key': 'value'}
|
||||
</parameter>
|
||||
</function>
|
||||
</tool_call>'''
|
||||
</tool_call>"""
|
||||
|
||||
request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools)
|
||||
extracted_tool_calls = qwen3_tool_parser_parametrized.extract_tool_calls(
|
||||
model_output, request=request)
|
||||
model_output, request=request
|
||||
)
|
||||
|
||||
args = json.loads(extracted_tool_calls.tool_calls[0].function.arguments)
|
||||
assert args["obj_param"] == {"key": "value"}
|
||||
|
||||
Reference in New Issue
Block a user