Skip to content

Commit 2bb9d45

Browse files
authored
[router] additional pythonic parser unit test (sgl-project#9730)
1 parent d0934a5 commit 2bb9d45

File tree

1 file changed

+310
-0
lines changed

1 file changed

+310
-0
lines changed

sgl-router/tests/tool_parser_pythonic.rs

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,313 @@ async fn test_pythonic_complex_nesting() {
247247
assert_eq!(args["operations"][0]["type"], "scale");
248248
assert_eq!(args["metadata"]["config"]["depth"], json!([1, 2, 3]));
249249
}
250+
251+
#[tokio::test]
252+
async fn test_parse_streaming_no_brackets() {
253+
// Test parsing text with no brackets (no tool calls)
254+
let parser = PythonicParser::new();
255+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
256+
257+
let text = "This is just normal text without any tool calls.";
258+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
259+
260+
match result {
261+
sglang_router_rs::tool_parser::StreamResult::Incomplete => {
262+
// Expected - no tool calls found
263+
assert_eq!(state.buffer, text);
264+
}
265+
_ => panic!("Should return Incomplete for text without tool calls"),
266+
}
267+
}
268+
269+
#[tokio::test]
270+
async fn test_parse_streaming_complete_tool_call() {
271+
// Test parsing a complete tool call
272+
let parser = PythonicParser::new();
273+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
274+
275+
let text = "Here's a tool call: [get_weather(location='New York', unit='celsius')]";
276+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
277+
278+
match result {
279+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => {
280+
assert_eq!(tool.function.name, "get_weather");
281+
let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap();
282+
assert_eq!(args["location"], "New York");
283+
assert_eq!(args["unit"], "celsius");
284+
assert_eq!(state.buffer, "");
285+
}
286+
_ => panic!("Should return ToolComplete for complete tool call"),
287+
}
288+
}
289+
290+
#[tokio::test]
291+
async fn test_parse_streaming_text_before_tool_call() {
292+
// Test parsing text that appears before a tool call
293+
let parser = PythonicParser::new();
294+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
295+
296+
let text = "This is some text before [get_weather(location='London')]";
297+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
298+
299+
match result {
300+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => {
301+
assert_eq!(tool.function.name, "get_weather");
302+
let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap();
303+
assert_eq!(args["location"], "London");
304+
}
305+
_ => panic!("Should return ToolComplete"),
306+
}
307+
}
308+
309+
#[tokio::test]
310+
async fn test_parse_streaming_partial_tool_call() {
311+
// Test parsing a partial tool call that spans multiple chunks
312+
let parser = PythonicParser::new();
313+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
314+
315+
// First chunk with opening bracket but no closing bracket
316+
let text1 = "Let me check the weather: [get_weather(location=";
317+
let result1 = parser.parse_incremental(text1, &mut state).await.unwrap();
318+
319+
match result1 {
320+
sglang_router_rs::tool_parser::StreamResult::Incomplete => {
321+
assert!(state.buffer.contains("[get_weather(location="));
322+
}
323+
_ => panic!("First chunk should return Incomplete"),
324+
}
325+
326+
// Second chunk completing the tool call
327+
let text2 = "'Paris')]";
328+
let result2 = parser.parse_incremental(text2, &mut state).await.unwrap();
329+
330+
match result2 {
331+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => {
332+
assert_eq!(tool.function.name, "get_weather");
333+
let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap();
334+
assert_eq!(args["location"], "Paris");
335+
assert_eq!(state.buffer, "");
336+
}
337+
_ => panic!("Second chunk should return ToolComplete"),
338+
}
339+
}
340+
341+
#[tokio::test]
342+
async fn test_parse_streaming_bracket_without_text_before() {
343+
// Test parsing a tool call that starts at the beginning of the text
344+
let parser = PythonicParser::new();
345+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
346+
347+
let text = "[search(query='python programming')]";
348+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
349+
350+
match result {
351+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => {
352+
assert_eq!(tool.function.name, "search");
353+
let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap();
354+
assert_eq!(args["query"], "python programming");
355+
}
356+
_ => panic!("Should return ToolComplete"),
357+
}
358+
}
359+
360+
#[tokio::test]
361+
async fn test_parse_streaming_text_after_tool_call() {
362+
// Test parsing text that appears after a tool call
363+
let parser = PythonicParser::new();
364+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
365+
366+
// First chunk with complete tool call and some text after
367+
let text = "[get_weather(location='Tokyo')] Here's the forecast:";
368+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
369+
370+
match result {
371+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => {
372+
assert_eq!(tool.function.name, "get_weather");
373+
// Text after tool call should remain in buffer
374+
// Note: Current implementation may clear buffer, this behavior needs verification
375+
}
376+
_ => panic!("Should return ToolComplete"),
377+
}
378+
}
379+
380+
#[tokio::test]
381+
async fn test_parse_streaming_multiple_tool_calls() {
382+
// Test parsing multiple tool calls in sequence
383+
let parser = PythonicParser::new();
384+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
385+
386+
let text = "[get_weather(location='Berlin'), search(query='restaurants')]";
387+
388+
// Current implementation may handle this as a single parse
389+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
390+
391+
// The parser should handle multiple tools in one bracket pair
392+
match result {
393+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(_) => {
394+
// Expected behavior - parses first tool
395+
}
396+
_ => {
397+
// Also acceptable if it returns Incomplete waiting for more
398+
}
399+
}
400+
}
401+
402+
#[tokio::test]
403+
async fn test_parse_streaming_opening_bracket_only() {
404+
// Test parsing text with only an opening bracket but no closing bracket
405+
let parser = PythonicParser::new();
406+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
407+
408+
let text = "Let's try this: [";
409+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
410+
411+
match result {
412+
sglang_router_rs::tool_parser::StreamResult::Incomplete => {
413+
assert!(state.buffer.ends_with("["));
414+
}
415+
_ => panic!("Should return Incomplete for partial bracket"),
416+
}
417+
}
418+
419+
#[tokio::test]
420+
async fn test_parse_streaming_nested_brackets() {
421+
// Test parsing tool calls with nested brackets in arguments
422+
let parser = PythonicParser::new();
423+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
424+
425+
let text = "[get_weather(location='New York', unit='celsius', data=[1, 2, 3])]";
426+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
427+
428+
match result {
429+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => {
430+
assert_eq!(tool.function.name, "get_weather");
431+
let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap();
432+
assert_eq!(args["location"], "New York");
433+
assert_eq!(args["unit"], "celsius");
434+
assert_eq!(args["data"], json!([1, 2, 3]));
435+
}
436+
_ => panic!("Should return ToolComplete"),
437+
}
438+
}
439+
440+
#[tokio::test]
441+
async fn test_parse_streaming_nested_brackets_dict() {
442+
// Test parsing tool calls with nested dictionaries and lists
443+
let parser = PythonicParser::new();
444+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
445+
446+
let text = r#"[search(query='test', config={'options': [1, 2], 'nested': {'key': 'value'}})]"#;
447+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
448+
449+
match result {
450+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => {
451+
assert_eq!(tool.function.name, "search");
452+
let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap();
453+
assert_eq!(args["query"], "test");
454+
assert_eq!(args["config"]["options"], json!([1, 2]));
455+
assert_eq!(args["config"]["nested"]["key"], "value");
456+
}
457+
_ => panic!("Should return ToolComplete"),
458+
}
459+
}
460+
461+
#[tokio::test]
462+
async fn test_parse_streaming_multiple_tools_with_nested_brackets() {
463+
// Test parsing multiple tool calls with nested brackets
464+
let parser = PythonicParser::new();
465+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
466+
467+
let text =
468+
"[get_weather(location='Paris', data=[10, 20]), search(query='test', filters=['a', 'b'])]";
469+
let result = parser.parse_incremental(text, &mut state).await.unwrap();
470+
471+
// Should parse both tools successfully
472+
match result {
473+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => {
474+
// At least gets the first tool
475+
assert_eq!(tool.function.name, "get_weather");
476+
}
477+
_ => panic!("Should return ToolComplete"),
478+
}
479+
}
480+
481+
#[tokio::test]
482+
async fn test_parse_streaming_partial_nested_brackets() {
483+
// Test parsing partial tool calls with nested brackets across chunks
484+
let parser = PythonicParser::new();
485+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
486+
487+
// First chunk with nested brackets but incomplete
488+
let text1 = "Here's a call: [get_weather(location='Tokyo', data=[1, 2";
489+
let result1 = parser.parse_incremental(text1, &mut state).await.unwrap();
490+
491+
match result1 {
492+
sglang_router_rs::tool_parser::StreamResult::Incomplete => {
493+
assert!(state
494+
.buffer
495+
.contains("[get_weather(location='Tokyo', data=[1, 2"));
496+
}
497+
_ => panic!("First chunk should return Incomplete"),
498+
}
499+
500+
// Second chunk completing the nested brackets
501+
let text2 = ", 3])]";
502+
let result2 = parser.parse_incremental(text2, &mut state).await.unwrap();
503+
504+
match result2 {
505+
sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => {
506+
assert_eq!(tool.function.name, "get_weather");
507+
let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap();
508+
assert_eq!(args["location"], "Tokyo");
509+
assert_eq!(args["data"], json!([1, 2, 3]));
510+
}
511+
_ => panic!("Second chunk should return ToolComplete"),
512+
}
513+
}
514+
515+
#[tokio::test]
516+
async fn test_parse_streaming_with_python_start_and_end_token() {
517+
// Test parsing a message that starts with <|python_start|> and <|python_end|> across chunks
518+
let parser = PythonicParser::new();
519+
let mut state = sglang_router_rs::tool_parser::ParseState::new();
520+
521+
let chunks = vec![
522+
"Here's a call: ",
523+
"<|python_",
524+
"start|>[get_weather(location=",
525+
"'Tokyo', data=[1, 2",
526+
", 3])]<|python_end|>",
527+
];
528+
529+
let mut got_tool = false;
530+
531+
for chunk in chunks {
532+
let result = parser.parse_incremental(chunk, &mut state).await.unwrap();
533+
if let sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) = result {
534+
assert_eq!(tool.function.name, "get_weather");
535+
let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap();
536+
assert_eq!(args["location"], "Tokyo");
537+
assert_eq!(args["data"], json!([1, 2, 3]));
538+
got_tool = true;
539+
}
540+
}
541+
542+
assert!(got_tool, "Should have parsed the tool call");
543+
}
544+
545+
#[tokio::test]
546+
async fn test_detect_and_parse_with_python_start_and_end_token() {
547+
// Test parsing a message that starts with <|python_start|> and contains a valid tool call
548+
let parser = PythonicParser::new();
549+
550+
let text = "User wants to get the weather in Mars. <|python_start|>[get_weather(location='Mars', unit='celsius')]<|python_end|> In this way we will get the weather in Mars.";
551+
let result = parser.parse_complete(text).await.unwrap();
552+
553+
assert_eq!(result.len(), 1);
554+
assert_eq!(result[0].function.name, "get_weather");
555+
556+
let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap();
557+
assert_eq!(args["location"], "Mars");
558+
assert_eq!(args["unit"], "celsius");
559+
}

0 commit comments

Comments
 (0)