bugfix(tool call ebnf): Fix EBNF generation for optional function parameters (#7283)
This commit is contained in:
@@ -515,7 +515,7 @@ class TestEBNFGeneration(unittest.TestCase):
|
||||
# Check that the EBNF contains expected patterns
|
||||
self.assertIn('call_get_weather ::= "get_weather" "(" ', ebnf)
|
||||
self.assertIn('"location" "=" basic_string', ebnf)
|
||||
self.assertIn('[ "unit" "=" ("\\"celsius\\"" | "\\"fahrenheit\\"") ]', ebnf)
|
||||
self.assertIn('( "unit" "=" ("\\"celsius\\"" | "\\"fahrenheit\\"") )', ebnf)
|
||||
|
||||
# Validate that the EBNF can be compiled by GrammarCompiler
|
||||
try:
|
||||
@@ -591,6 +591,224 @@ class TestEBNFGeneration(unittest.TestCase):
|
||||
except RuntimeError as e:
|
||||
self.fail(f"Failed to compile EBNF: {e}")
|
||||
|
||||
def test_weather_function_optional_parameter_handling(self):
|
||||
"""Test that weather function with optional unit parameter generates correct EBNF without trailing commas."""
|
||||
# Create a weather tool with required location and optional unit
|
||||
weather_tool = Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="get_current_weather",
|
||||
description="Get the current weather in a given location",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "The city and state, e.g. San Francisco, CA",
|
||||
},
|
||||
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
|
||||
},
|
||||
"required": ["location"],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Test all detectors with the weather tool
|
||||
detectors = {
|
||||
"pythonic": self.pythonic_detector,
|
||||
"deepseekv3": self.deepseekv3_detector,
|
||||
"llama32": self.llama32_detector,
|
||||
"mistral": self.mistral_detector,
|
||||
"qwen25": self.qwen25_detector,
|
||||
}
|
||||
|
||||
for name, detector in detectors.items():
|
||||
with self.subTest(detector=name):
|
||||
ebnf = detector.build_ebnf([weather_tool])
|
||||
self.assertIsNotNone(ebnf, f"{name} detector should generate EBNF")
|
||||
|
||||
# Check that the EBNF properly handles optional parameters
|
||||
if name == "pythonic":
|
||||
# Pythonic format: location="Paris" ( , ( unit=("celsius" | "fahrenheit") )?
|
||||
self.assertIn('"location" "=" basic_string', ebnf)
|
||||
# The comma should be inside the optional brackets for unit
|
||||
self.assertIn('( "," ( "unit" "=" ', ebnf)
|
||||
else:
|
||||
# JSON format: "location": "Paris" ( , ( "unit": ("celsius" | "fahrenheit") )?
|
||||
self.assertIn('"location\\"" ":" basic_string', ebnf)
|
||||
# The comma should be part of the optional group
|
||||
# This pattern ensures no trailing comma when unit is omitted
|
||||
self.assertIn('( "," ( "\\"unit\\"" ":"', ebnf)
|
||||
|
||||
# Validate that the EBNF can be compiled
|
||||
try:
|
||||
ctx = self.grammar_compiler.compile_grammar(ebnf)
|
||||
self.assertIsNotNone(
|
||||
ctx, f"{name} EBNF should compile successfully"
|
||||
)
|
||||
except RuntimeError as e:
|
||||
self.fail(f"Failed to compile {name} EBNF: {e}")
|
||||
|
||||
def test_multiple_optional_parameters_flexible_ordering(self):
|
||||
"""Test that multiple optional parameters allow flexible ordering using llama.cpp approach."""
|
||||
# Create a tool with one required and multiple optional parameters
|
||||
test_tool = Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="test_func",
|
||||
description="Test function with multiple optional parameters",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"required_field": {"type": "string"},
|
||||
"opt1": {"type": "number"},
|
||||
"opt2": {"type": "boolean"},
|
||||
"opt3": {"type": "string"},
|
||||
},
|
||||
"required": ["required_field"],
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Test JSON-based detectors (not pythonic)
|
||||
json_detectors = {
|
||||
"deepseekv3": self.deepseekv3_detector,
|
||||
"llama32": self.llama32_detector,
|
||||
"mistral": self.mistral_detector,
|
||||
"qwen25": self.qwen25_detector,
|
||||
}
|
||||
|
||||
for name, detector in json_detectors.items():
|
||||
with self.subTest(detector=name):
|
||||
ebnf = detector.build_ebnf([test_tool])
|
||||
self.assertIsNotNone(ebnf, f"{name} detector should generate EBNF")
|
||||
|
||||
# Print the arguments rule for debugging
|
||||
lines = ebnf.split("\n")
|
||||
args_rule = None
|
||||
for line in lines:
|
||||
if line.startswith("arguments_test_func ::="):
|
||||
args_rule = line
|
||||
break
|
||||
|
||||
self.assertIsNotNone(
|
||||
args_rule, f"{name} should have arguments_test_func rule"
|
||||
)
|
||||
|
||||
# Check required field
|
||||
self.assertIn('"required_field\\"" ":" basic_string', ebnf)
|
||||
|
||||
# Check the structure for optional parameters
|
||||
# The pattern should be: required_field ( "," ( opt1 ... | opt2 ... | opt3 ... ) )?
|
||||
# This allows flexible ordering where any optional can be first
|
||||
|
||||
# Check that optional parameters are in a group with comma
|
||||
if args_rule: # Only check if args_rule was found
|
||||
self.assertIn(
|
||||
'( ","',
|
||||
args_rule,
|
||||
f"{name} should have comma grouped with optional parameters",
|
||||
)
|
||||
|
||||
# Check for the alternation pattern that allows flexible ordering
|
||||
# Should contain patterns like: opt1 ... | opt2 ... | opt3
|
||||
self.assertIn('"opt1\\"" ":" basic_number', args_rule)
|
||||
self.assertIn('"opt2\\"" ":" basic_boolean', args_rule)
|
||||
self.assertIn('"opt3\\"" ":" basic_string', args_rule)
|
||||
|
||||
# Check for alternation (|) which allows skipping optional parameters
|
||||
self.assertIn(
|
||||
"|",
|
||||
args_rule,
|
||||
f"{name} should use alternation for flexible optional ordering",
|
||||
)
|
||||
|
||||
# Check that the pattern ends properly with closing braces
|
||||
self.assertTrue(
|
||||
args_rule.endswith('"}"'),
|
||||
f"{name} arguments rule should end with closing brace",
|
||||
)
|
||||
|
||||
# Validate compilation
|
||||
try:
|
||||
ctx = self.grammar_compiler.compile_grammar(ebnf)
|
||||
self.assertIsNotNone(
|
||||
ctx, f"{name} EBNF should compile successfully"
|
||||
)
|
||||
except RuntimeError as e:
|
||||
self.fail(f"Failed to compile {name} EBNF: {e}")
|
||||
|
||||
def test_all_optional_parameters_ordering(self):
|
||||
"""Test the behavior when ALL parameters are optional - verifies ordering constraints."""
|
||||
# Create a tool with only optional parameters
|
||||
all_optional_tool = Tool(
|
||||
type="function",
|
||||
function=Function(
|
||||
name="optional_func",
|
||||
description="Function with all optional parameters",
|
||||
parameters={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"opt1": {"type": "string"},
|
||||
"opt2": {"type": "number"},
|
||||
"opt3": {"type": "boolean"},
|
||||
},
|
||||
"required": [], # No required parameters
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
# Test JSON-based detectors
|
||||
json_detectors = {
|
||||
"deepseekv3": self.deepseekv3_detector,
|
||||
"llama32": self.llama32_detector,
|
||||
"mistral": self.mistral_detector,
|
||||
"qwen25": self.qwen25_detector,
|
||||
}
|
||||
|
||||
for name, detector in json_detectors.items():
|
||||
with self.subTest(detector=name):
|
||||
ebnf = detector.build_ebnf([all_optional_tool])
|
||||
self.assertIsNotNone(ebnf, f"{name} detector should generate EBNF")
|
||||
|
||||
# Extract the arguments rule
|
||||
lines = ebnf.split("\n")
|
||||
args_rule = None
|
||||
for line in lines:
|
||||
if line.startswith("arguments_optional_func ::="):
|
||||
args_rule = line
|
||||
break
|
||||
|
||||
self.assertIsNotNone(
|
||||
args_rule, f"{name} should have arguments_optional_func rule"
|
||||
)
|
||||
|
||||
if args_rule:
|
||||
# When all parameters are optional, the pattern now uses alternation:
|
||||
# "{" ( opt1 ... | opt2 ... | opt3 ... )? "}"
|
||||
# This allows flexible ordering where any optional can appear first
|
||||
|
||||
# Check the structure
|
||||
self.assertIn('"opt1\\"" ":" basic_string', args_rule)
|
||||
self.assertIn('"opt2\\"" ":" basic_number', args_rule)
|
||||
self.assertIn('"opt3\\"" ":" basic_boolean', args_rule)
|
||||
|
||||
# The pattern SHOULD have alternation (|) for flexible ordering
|
||||
self.assertIn(
|
||||
"|",
|
||||
args_rule,
|
||||
f"{name} should use alternation for flexible ordering even when all properties are optional",
|
||||
)
|
||||
|
||||
# Validate compilation
|
||||
try:
|
||||
ctx = self.grammar_compiler.compile_grammar(ebnf)
|
||||
self.assertIsNotNone(
|
||||
ctx, f"{name} EBNF should compile successfully"
|
||||
)
|
||||
except RuntimeError as e:
|
||||
self.fail(f"Failed to compile {name} EBNF: {e}")
|
||||
|
||||
|
||||
class TestBaseFormatDetector(unittest.TestCase):
|
||||
"""Test buffer management and sequential tool index assignment in BaseFormatDetector."""
|
||||
|
||||
Reference in New Issue
Block a user