@@ -247,3 +247,313 @@ async fn test_pythonic_complex_nesting() {
247
247
assert_eq ! ( args[ "operations" ] [ 0 ] [ "type" ] , "scale" ) ;
248
248
assert_eq ! ( args[ "metadata" ] [ "config" ] [ "depth" ] , json!( [ 1 , 2 , 3 ] ) ) ;
249
249
}
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