| 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. |
| """ |
| |
| |
| 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) |
| |
| self._accumulate_tool_calls(tool_calls, result) |
|
|
| except Exception as e: |
| errors.append(f"Chunk {i} ({repr(chunk)}): {type(e).__name__}: {e}") |
|
|
| |
| if errors: |
| self.fail("Errors occurred during parsing:\n" + "\n".join(errors)) |
|
|
| |
| self.assertEqual(len(tool_calls), 2, "Should have parsed exactly 2 tool calls") |
|
|
| |
| 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" |
| ) |
|
|
| |
| 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) |
|
|
| |
| 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() |
|
|