mirror of
https://github.com/kvcache-ai/sglang.git
synced 2026-07-07 07:47:25 +00:00
164 lines
5.7 KiB
Python
164 lines
5.7 KiB
Python
import json
|
|
|
|
"""
|
|
Test case for parallel tool call parsing.
|
|
|
|
This test verifies that the parser correctly handles parallel tool calls
|
|
with array parameters in JSON array format.
|
|
|
|
Scenario:
|
|
- Model outputs two parallel tool calls in JSON array format
|
|
- Both tools have array parameters (e.g., "title": ["7.8.9 H-9 ..."])
|
|
- First tool completes with closing braces
|
|
- Second tool starts with opening brace
|
|
- The parser must correctly handle the '[' characters in array parameters
|
|
without confusing them with the JSON array start
|
|
|
|
Expected behavior: Both tools should be parsed correctly.
|
|
"""
|
|
|
|
import unittest
|
|
|
|
from sglang.srt.entrypoints.openai.protocol import Function, Tool
|
|
from sglang.srt.function_call.json_array_parser import JsonArrayParser
|
|
from sglang.test.ci.ci_register import register_cpu_ci
|
|
|
|
register_cpu_ci(1.0, "default")
|
|
|
|
|
|
class TestParallelToolCalls(unittest.TestCase):
|
|
"""Test case for parallel tool call parsing with array parameters."""
|
|
|
|
def setUp(self):
|
|
"""Set up test tools and detector."""
|
|
self.tools = [
|
|
Tool(
|
|
type="function",
|
|
function=Function(
|
|
name="search_docs",
|
|
description="Search documents",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"title": {
|
|
"type": "array",
|
|
"items": {"type": "string"},
|
|
"description": "Document title",
|
|
}
|
|
},
|
|
"required": ["title"],
|
|
},
|
|
),
|
|
),
|
|
]
|
|
self.detector = JsonArrayParser()
|
|
|
|
def _accumulate_tool_calls(self, tool_calls, result):
|
|
"""Helper method to accumulate tool call results from parsing output."""
|
|
if not result.calls:
|
|
return
|
|
for call in result.calls:
|
|
if call.tool_index is None:
|
|
continue
|
|
while len(tool_calls) <= call.tool_index:
|
|
tool_calls.append({"name": "", "parameters": ""})
|
|
if call.name:
|
|
tool_calls[call.tool_index]["name"] = call.name
|
|
if call.parameters:
|
|
tool_calls[call.tool_index]["parameters"] += call.parameters
|
|
|
|
def test_parallel_tool_calls_with_array_parameters(self):
|
|
"""
|
|
Test parsing two parallel tool calls where both have array parameters.
|
|
|
|
This test reproduces the specific scenario:
|
|
- Two tool calls separated by comma
|
|
- Both tools have array parameters containing '[' character
|
|
- First tool completes with '}},'
|
|
- Second tool starts with '{"name": ..., "parameters": {"title": ["'
|
|
|
|
Expected: Both tools should be parsed correctly without errors.
|
|
"""
|
|
# Simulate more realistic streaming chunks where
|
|
# the key issue is the comma separator followed by second tool with array param
|
|
chunks = [
|
|
"[\n",
|
|
' {"name": "search_docs", "parameters": {"title": ["7.8.9"',
|
|
'], "filename": "doc1"}},\n',
|
|
' {"name": "search_docs", "parameters": {"title": ',
|
|
'["4.8"], "filename": "doc2"}}',
|
|
"]",
|
|
]
|
|
|
|
tool_calls = []
|
|
errors = []
|
|
|
|
for i, chunk in enumerate(chunks):
|
|
try:
|
|
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
|
# Collect tool calls
|
|
self._accumulate_tool_calls(tool_calls, result)
|
|
|
|
except Exception as e:
|
|
errors.append(f"Chunk {i} ({repr(chunk)}): {type(e).__name__}: {e}")
|
|
|
|
# Verify no errors occurred
|
|
if errors:
|
|
self.fail("Errors occurred during parsing:\n" + "\n".join(errors))
|
|
|
|
# Verify both tool calls were parsed
|
|
self.assertEqual(len(tool_calls), 2, "Should have parsed exactly 2 tool calls")
|
|
|
|
# Verify first tool call
|
|
self.assertEqual(
|
|
tool_calls[0]["name"],
|
|
"search_docs",
|
|
"First tool name should be search_docs",
|
|
)
|
|
params1 = json.loads(tool_calls[0]["parameters"])
|
|
self.assertEqual(params1["title"], ["7.8.9"], "First tool title should match")
|
|
self.assertEqual(
|
|
params1["filename"], "doc1", "First tool filename should be doc1"
|
|
)
|
|
|
|
# Verify second tool call
|
|
self.assertEqual(
|
|
tool_calls[1]["name"],
|
|
"search_docs",
|
|
"Second tool name should be search_docs",
|
|
)
|
|
params2 = json.loads(tool_calls[1]["parameters"])
|
|
self.assertEqual(params2["title"], ["4.8"], "Second tool title should match")
|
|
self.assertEqual(
|
|
params2["filename"], "doc2", "Second tool filename should be doc2"
|
|
)
|
|
|
|
def test_simple_parallel_tool_calls(self):
|
|
"""
|
|
Test a simpler case of two parallel tool calls with array parameters.
|
|
|
|
This is a minimal test case that still tests the core functionality.
|
|
"""
|
|
chunks = [
|
|
"[\n",
|
|
' {"name": "search_docs", "parameters": {"title": ["a"]}},',
|
|
"\n",
|
|
' {"name": "search_docs", "parameters": {"title": ["b"]}}',
|
|
"]",
|
|
]
|
|
|
|
tool_calls = []
|
|
|
|
for chunk in chunks:
|
|
result = self.detector.parse_streaming_increment(chunk, self.tools)
|
|
self._accumulate_tool_calls(tool_calls, result)
|
|
|
|
# Should parse both tools successfully
|
|
self.assertEqual(len(tool_calls), 2, "Should parse 2 tool calls")
|
|
self.assertEqual(tool_calls[0]["name"], "search_docs")
|
|
self.assertEqual(tool_calls[1]["name"], "search_docs")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|