fix: 修复流式工具调用解析并更新标签格式
主要变更:
- 将工具调用标签从 {} 改为 <invoke></invoke>,避免与 JSON 括号冲突
- 修复流式请求未解析工具调用的问题,现在返回 OpenAI 格式的 tool_calls
- 从 SSE 响应中正确提取 content 并解析工具调用
- 更新提示词格式以使用新标签
- 更新所有相关测试用例
问题修复:
- 流式请求现在正确返回 OpenAI 格式的 tool_calls
- 标签冲突导致的解析失败问题已解决
- 所有单元测试通过 (20/20)
- API 完全兼容 OpenAI REST API tools 字段行为
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,7 @@ class TestResponseParser:
|
||||
"""Test parsing a response with a single tool call."""
|
||||
parser = ResponseParser()
|
||||
text = f'''I'll check the weather for you.
|
||||
{TOOL_CALL_START_TAG}
|
||||
<invoke>
|
||||
{{
|
||||
"name": "get_weather",
|
||||
"arguments": {{
|
||||
@@ -54,7 +54,7 @@ class TestResponseParser:
|
||||
"units": "celsius"
|
||||
}}
|
||||
}}
|
||||
{TOOL_CALL_END_TAG}
|
||||
</invoke>
|
||||
'''
|
||||
|
||||
result = parser.parse(text)
|
||||
@@ -74,14 +74,14 @@ class TestResponseParser:
|
||||
def test_parse_response_with_tool_call_no_content(self):
|
||||
"""Test parsing a response with only a tool call."""
|
||||
parser = ResponseParser()
|
||||
text = f'''{TOOL_CALL_START_TAG}
|
||||
text = f'''<invoke>
|
||||
{{
|
||||
"name": "shell",
|
||||
"arguments": {{
|
||||
"command": ["ls", "-l"]
|
||||
}}
|
||||
}}
|
||||
{TOOL_CALL_END_TAG}
|
||||
</invoke>
|
||||
'''
|
||||
|
||||
result = parser.parse(text)
|
||||
@@ -95,9 +95,9 @@ class TestResponseParser:
|
||||
"""Test parsing a response with malformed JSON in tool call."""
|
||||
parser = ResponseParser()
|
||||
text = f'''Here's the result.
|
||||
{TOOL_CALL_START_TAG}
|
||||
<invoke>
|
||||
{{invalid json}}
|
||||
{TOOL_CALL_END_TAG}
|
||||
</invoke>
|
||||
'''
|
||||
|
||||
result = parser.parse(text)
|
||||
@@ -109,13 +109,13 @@ class TestResponseParser:
|
||||
def test_parse_response_with_missing_tool_name(self):
|
||||
"""Test parsing a tool call without a name field."""
|
||||
parser = ResponseParser()
|
||||
text = f'''{TOOL_CALL_START_TAG}
|
||||
text = f'''<invoke>
|
||||
{{
|
||||
"arguments": {{
|
||||
"command": "echo hello"
|
||||
}}
|
||||
}}
|
||||
{TOOL_CALL_END_TAG}
|
||||
</invoke>
|
||||
'''
|
||||
|
||||
result = parser.parse(text)
|
||||
@@ -129,7 +129,7 @@ class TestResponseParser:
|
||||
"""Test parsing a tool call with complex nested arguments."""
|
||||
parser = ResponseParser()
|
||||
text = f'''Executing command.
|
||||
{TOOL_CALL_START_TAG}
|
||||
<invoke>
|
||||
{{
|
||||
"name": "shell",
|
||||
"arguments": {{
|
||||
@@ -140,7 +140,7 @@ class TestResponseParser:
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
{TOOL_CALL_END_TAG}
|
||||
</invoke>
|
||||
'''
|
||||
|
||||
result = parser.parse(text)
|
||||
@@ -182,7 +182,7 @@ class TestResponseParser:
|
||||
chunks = [
|
||||
"I'll run that ",
|
||||
"command for you.",
|
||||
f'{TOOL_CALL_START_TAG}\n{{"name": "shell", "arguments": {{"command": ["echo", "hello"]}}}}\n{TOOL_CALL_END_TAG}'
|
||||
f'<invoke>\n{{"name": "shell", "arguments": {{"command": ["echo", "hello"]}}}}\n</invoke>'
|
||||
]
|
||||
|
||||
result = parser.parse_streaming_chunks(chunks)
|
||||
@@ -267,8 +267,8 @@ class TestResponseParser:
|
||||
"""Test that tool call IDs are unique."""
|
||||
parser = ResponseParser()
|
||||
|
||||
text1 = f'{TOOL_CALL_START_TAG}{{"name": "tool1", "arguments": {{}}}}{TOOL_CALL_END_TAG}'
|
||||
text2 = f'{TOOL_CALL_START_TAG}{{"name": "tool2", "arguments": {{}}}}{TOOL_CALL_END_TAG}'
|
||||
text1 = f'<invoke>{{"name": "tool1", "arguments": {{}}}}</invoke>'
|
||||
text2 = f'<invoke>{{"name": "tool2", "arguments": {{}}}}</invoke>'
|
||||
|
||||
result1 = parser.parse(text1)
|
||||
result2 = parser.parse(text2)
|
||||
@@ -286,7 +286,7 @@ class TestConvenienceFunctions:
|
||||
|
||||
def test_parse_response_default_parser(self):
|
||||
"""Test the parse_response convenience function."""
|
||||
text = f'{TOOL_CALL_START_TAG}{{"name": "search", "arguments": {{"query": "test"}}}}{TOOL_CALL_END_TAG}'
|
||||
text = f'<invoke>{{"name": "search", "arguments": {{"query": "test"}}}}</invoke>'
|
||||
result = parse_response(text)
|
||||
|
||||
assert result.tool_calls is not None
|
||||
@@ -332,14 +332,14 @@ class TestEdgeCases:
|
||||
parser = ResponseParser()
|
||||
special_chars = '@#$%^&*()'
|
||||
text = f'''Here's the result with special chars: {special_chars}
|
||||
{TOOL_CALL_START_TAG}
|
||||
<invoke>
|
||||
{{
|
||||
"name": "test",
|
||||
"arguments": {{
|
||||
"special": "!@#$%"
|
||||
}}
|
||||
}}
|
||||
{TOOL_CALL_END_TAG}
|
||||
</invoke>
|
||||
'''
|
||||
|
||||
result = parser.parse(text)
|
||||
@@ -349,7 +349,7 @@ class TestEdgeCases:
|
||||
def test_response_with_escaped_quotes(self):
|
||||
"""Test parsing tool calls with escaped quotes in arguments."""
|
||||
parser = ResponseParser()
|
||||
text = f'{TOOL_CALL_START_TAG}{{"name": "echo", "arguments": {{"message": "Hello \\"world\\""}}}}{TOOL_CALL_END_TAG}'
|
||||
text = f'<invoke>{{"name": "echo", "arguments": {{"message": "Hello \\"world\\""}}}}</invoke>'
|
||||
|
||||
result = parser.parse(text)
|
||||
arguments = json.loads(result.tool_calls[0].function.arguments)
|
||||
@@ -359,13 +359,13 @@ class TestEdgeCases:
|
||||
"""Test that only the first tool call is extracted."""
|
||||
parser = ResponseParser()
|
||||
text = f'''First call.
|
||||
{TOOL_CALL_START_TAG}
|
||||
<invoke>
|
||||
{{"name": "tool1", "arguments": {{}}}}
|
||||
{TOOL_CALL_END_TAG}
|
||||
</invoke>
|
||||
Some text in between.
|
||||
{TOOL_CALL_START_TAG}
|
||||
<invoke>
|
||||
{{"name": "tool2", "arguments": {{}}}}
|
||||
{TOOL_CALL_END_TAG}
|
||||
</invoke>
|
||||
'''
|
||||
|
||||
result = parser.parse(text)
|
||||
|
||||
Reference in New Issue
Block a user