mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
fix: normalize malformed <parameter> tags (Qwen3) (#10539)
This commit is contained in:
parent
adb773789a
commit
b311ae6e15
@ -705,6 +705,25 @@ def _fix_stopword(content: str) -> str:
|
||||
return content
|
||||
|
||||
|
||||
def _normalize_parameter_tags(fn_body: str) -> str:
|
||||
"""Normalize malformed parameter tags to the canonical format.
|
||||
|
||||
Some models occasionally emit malformed parameter tags like:
|
||||
<parameter=command=str_replace</parameter>
|
||||
instead of the correct:
|
||||
<parameter=command>str_replace</parameter>
|
||||
|
||||
This function rewrites the malformed form into the correct one to allow
|
||||
downstream parsing to succeed.
|
||||
"""
|
||||
# Replace '<parameter=name=value</parameter>' with '<parameter=name>value</parameter>'
|
||||
return re.sub(
|
||||
r'<parameter=([a-zA-Z0-9_]+)=([^<]*)</parameter>',
|
||||
r'<parameter=\1>\2</parameter>',
|
||||
fn_body,
|
||||
)
|
||||
|
||||
|
||||
def convert_non_fncall_messages_to_fncall_messages(
|
||||
messages: list[dict],
|
||||
tools: list[ChatCompletionToolParam],
|
||||
@ -852,7 +871,7 @@ def convert_non_fncall_messages_to_fncall_messages(
|
||||
|
||||
if fn_match:
|
||||
fn_name = fn_match.group(1)
|
||||
fn_body = fn_match.group(2)
|
||||
fn_body = _normalize_parameter_tags(fn_match.group(2))
|
||||
matching_tool = next(
|
||||
(
|
||||
tool['function']
|
||||
|
||||
@ -96,6 +96,52 @@ FNCALL_TOOLS: list[ChatCompletionToolParam] = [
|
||||
]
|
||||
|
||||
|
||||
def test_malformed_parameter_parsing_recovery():
|
||||
"""Ensure we can recover when models emit malformed parameter tags like <parameter=command=str_replace</parameter>.
|
||||
|
||||
This simulates a tool call to str_replace_editor where the 'command' parameter is malformed.
|
||||
"""
|
||||
from openhands.llm.fn_call_converter import (
|
||||
convert_non_fncall_messages_to_fncall_messages,
|
||||
)
|
||||
|
||||
# Construct an assistant message with malformed parameter tag for 'command'
|
||||
assistant_message = {
|
||||
'role': 'assistant',
|
||||
'content': (
|
||||
'<function=str_replace_editor>\n'
|
||||
'<parameter=command=str_replace</parameter>\n' # malformed form
|
||||
'<parameter=path>/repo/app.py</parameter>\n'
|
||||
'<parameter=old_str>foo</parameter>\n'
|
||||
'<parameter=new_str>bar</parameter>\n'
|
||||
'</function>'
|
||||
),
|
||||
}
|
||||
|
||||
messages = [
|
||||
{'role': 'system', 'content': 'test'},
|
||||
{'role': 'user', 'content': 'do edit'},
|
||||
assistant_message,
|
||||
]
|
||||
|
||||
converted = convert_non_fncall_messages_to_fncall_messages(messages, FNCALL_TOOLS)
|
||||
|
||||
# The last message should be assistant with a parsed tool call
|
||||
last = converted[-1]
|
||||
assert last['role'] == 'assistant'
|
||||
assert 'tool_calls' in last and len(last['tool_calls']) == 1
|
||||
tool_call = last['tool_calls'][0]
|
||||
assert tool_call['type'] == 'function'
|
||||
assert tool_call['function']['name'] == 'str_replace_editor'
|
||||
|
||||
# Arguments must be a valid JSON with command=str_replace and proper params
|
||||
args = json.loads(tool_call['function']['arguments'])
|
||||
assert args['command'] == 'str_replace'
|
||||
assert args['path'] == '/repo/app.py'
|
||||
assert args['old_str'] == 'foo'
|
||||
assert args['new_str'] == 'bar'
|
||||
|
||||
|
||||
def test_convert_tools_to_description():
|
||||
formatted_tools = convert_tools_to_description(FNCALL_TOOLS)
|
||||
print(formatted_tools)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user