//! Pythonic Parser Integration Tests //! //! Tests for the Pythonic parser which handles Python function call syntax use serde_json::json; use sglang_router_rs::tool_parser::{PythonicParser, ToolParser}; #[tokio::test] async fn test_pythonic_single_function() { let parser = PythonicParser::new(); let input = r#"[get_weather(city="London", units="celsius")]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].function.name, "get_weather"); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["city"], "London"); assert_eq!(args["units"], "celsius"); } #[tokio::test] async fn test_pythonic_multiple_functions() { let parser = PythonicParser::new(); let input = r#"[search_web(query="Rust programming", max_results=5), get_time(timezone="UTC")]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 2); assert_eq!(result[0].function.name, "search_web"); assert_eq!(result[1].function.name, "get_time"); let args0: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args0["query"], "Rust programming"); assert_eq!(args0["max_results"], 5); } #[tokio::test] async fn test_pythonic_with_python_literals() { let parser = PythonicParser::new(); let input = r#"[configure(enabled=True, disabled=False, optional=None)]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["enabled"], true); assert_eq!(args["disabled"], false); assert_eq!(args["optional"], json!(null)); } #[tokio::test] async fn test_pythonic_with_lists_and_dicts() { let parser = PythonicParser::new(); let input = r#"[process_data(items=[1, 2, 3], config={"key": "value", "nested": {"deep": True}})]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["items"], json!([1, 2, 3])); assert_eq!(args["config"]["key"], "value"); assert_eq!(args["config"]["nested"]["deep"], true); } #[tokio::test] async fn test_pythonic_with_special_tokens() { let parser = PythonicParser::new(); // Llama 4 sometimes outputs these tokens let input = r#"<|python_start|>[calculate(x=10, y=20)]<|python_end|>"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].function.name, "calculate"); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["x"], 10); assert_eq!(args["y"], 20); } #[tokio::test] async fn test_pythonic_with_nested_parentheses() { let parser = PythonicParser::new(); let input = r#"[math_eval(expression="(2 + 3) * (4 - 1)", round_to=2)]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["expression"], "(2 + 3) * (4 - 1)"); assert_eq!(args["round_to"], 2); } #[tokio::test] async fn test_pythonic_with_escaped_quotes() { let parser = PythonicParser::new(); let input = r#"[echo(text="She said \"Hello\" to him")]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["text"], "She said \"Hello\" to him"); } #[tokio::test] async fn test_pythonic_empty_arguments() { let parser = PythonicParser::new(); let input = r#"[ping()]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].function.name, "ping"); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args, json!({})); } #[tokio::test] async fn test_pythonic_format_detection() { let parser = PythonicParser::new(); assert!(parser.detect_format("[function_name(")); assert!(parser.detect_format("[get_weather(city=\"NYC\")]")); assert!(!parser.detect_format("Just plain text")); assert!(!parser.detect_format("[1, 2, 3]")); // Plain list assert!(!parser.detect_format("{\"name\": \"test\"}")); // JSON } #[tokio::test] async fn test_pythonic_invalid_syntax() { let parser = PythonicParser::new(); // Missing closing bracket let input = r#"[function(arg=value"#; if let Ok(result) = parser.parse_complete(input).await { assert_eq!(result.len(), 0); } // Error is also acceptable for invalid syntax // Invalid Python syntax - empty parameter name // Note: The parser currently accepts this invalid syntax and returns a result // This is a known limitation of the current implementation let input = r#"[function(=value)]"#; if let Ok(result) = parser.parse_complete(input).await { // The parser incorrectly accepts this, returning 1 result // We'll accept this behavior for now but note it's not ideal assert!(result.len() <= 1, "Should parse at most one function"); } // Error would be the correct behavior } #[tokio::test] async fn test_pythonic_real_world_llama4() { let parser = PythonicParser::new(); // Actual output from Llama 4 model let input = r#"I'll help you with multiple tasks. Let me search for information and perform calculations. [web_search(query="latest Rust features", max_results=3, safe_search=True), calculate(expression="42 * 3.14159", precision=2), get_weather(city="San Francisco", units="fahrenheit", include_forecast=False)] These functions will provide the information you need."#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 3); assert_eq!(result[0].function.name, "web_search"); assert_eq!(result[1].function.name, "calculate"); assert_eq!(result[2].function.name, "get_weather"); let args0: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args0["query"], "latest Rust features"); assert_eq!(args0["safe_search"], true); } #[tokio::test] async fn test_pythonic_nested_brackets_in_lists() { let parser = PythonicParser::new(); // Test nested brackets within list arguments let input = r#"[process_matrix(data=[[1, 2], [3, 4]], labels=["row[0]", "row[1]"])]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].function.name, "process_matrix"); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["data"], json!([[1, 2], [3, 4]])); assert_eq!(args["labels"], json!(["row[0]", "row[1]"])); } #[tokio::test] async fn test_pythonic_nested_brackets_in_dicts() { let parser = PythonicParser::new(); // Test nested brackets within dictionary arguments let input = r#"[analyze(config={"patterns": ["[a-z]+", "[0-9]+"], "nested": {"list": [1, [2, 3]]}})]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].function.name, "analyze"); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["config"]["patterns"], json!(["[a-z]+", "[0-9]+"])); assert_eq!(args["config"]["nested"]["list"], json!([1, [2, 3]])); } #[tokio::test] async fn test_pythonic_mixed_quotes() { let parser = PythonicParser::new(); // Test mixed quote types in arguments let input = r#"[format_text(single='Hello', double="World", mixed="It's \"quoted\"")]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].function.name, "format_text"); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["single"], "Hello"); assert_eq!(args["double"], "World"); assert_eq!(args["mixed"], "It's \"quoted\""); } #[tokio::test] async fn test_pythonic_complex_nesting() { let parser = PythonicParser::new(); // Test complex nested structures let input = r#"[transform( matrix=[[1, [2, 3]], [4, [5, [6, 7]]]], operations=[{"type": "scale", "factor": [2, 3]}, {"type": "rotate", "angle": 90}], metadata={"tags": ["nested[0]", "nested[1]"], "config": {"depth": [1, 2, 3]}} )]"#; let result = parser.parse_complete(input).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].function.name, "transform"); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert!(args["matrix"].is_array()); assert!(args["operations"].is_array()); assert_eq!(args["operations"][0]["type"], "scale"); assert_eq!(args["metadata"]["config"]["depth"], json!([1, 2, 3])); } #[tokio::test] async fn test_parse_streaming_no_brackets() { // Test parsing text with no brackets (no tool calls) let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let text = "This is just normal text without any tool calls."; let result = parser.parse_incremental(text, &mut state).await.unwrap(); match result { sglang_router_rs::tool_parser::StreamResult::Incomplete => { // Expected - no tool calls found assert_eq!(state.buffer, text); } _ => panic!("Should return Incomplete for text without tool calls"), } } #[tokio::test] async fn test_parse_streaming_complete_tool_call() { // Test parsing a complete tool call let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let text = "Here's a tool call: [get_weather(location='New York', unit='celsius')]"; let result = parser.parse_incremental(text, &mut state).await.unwrap(); match result { sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => { assert_eq!(tool.function.name, "get_weather"); let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap(); assert_eq!(args["location"], "New York"); assert_eq!(args["unit"], "celsius"); assert_eq!(state.buffer, ""); } _ => panic!("Should return ToolComplete for complete tool call"), } } #[tokio::test] async fn test_parse_streaming_text_before_tool_call() { // Test parsing text that appears before a tool call let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let text = "This is some text before [get_weather(location='London')]"; let result = parser.parse_incremental(text, &mut state).await.unwrap(); match result { sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => { assert_eq!(tool.function.name, "get_weather"); let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap(); assert_eq!(args["location"], "London"); } _ => panic!("Should return ToolComplete"), } } #[tokio::test] async fn test_parse_streaming_partial_tool_call() { // Test parsing a partial tool call that spans multiple chunks let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); // First chunk with opening bracket but no closing bracket let text1 = "Let me check the weather: [get_weather(location="; let result1 = parser.parse_incremental(text1, &mut state).await.unwrap(); match result1 { sglang_router_rs::tool_parser::StreamResult::Incomplete => { assert!(state.buffer.contains("[get_weather(location=")); } _ => panic!("First chunk should return Incomplete"), } // Second chunk completing the tool call let text2 = "'Paris')]"; let result2 = parser.parse_incremental(text2, &mut state).await.unwrap(); match result2 { sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => { assert_eq!(tool.function.name, "get_weather"); let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap(); assert_eq!(args["location"], "Paris"); assert_eq!(state.buffer, ""); } _ => panic!("Second chunk should return ToolComplete"), } } #[tokio::test] async fn test_parse_streaming_bracket_without_text_before() { // Test parsing a tool call that starts at the beginning of the text let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let text = "[search(query='python programming')]"; let result = parser.parse_incremental(text, &mut state).await.unwrap(); match result { sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => { assert_eq!(tool.function.name, "search"); let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap(); assert_eq!(args["query"], "python programming"); } _ => panic!("Should return ToolComplete"), } } #[tokio::test] async fn test_parse_streaming_text_after_tool_call() { // Test parsing text that appears after a tool call let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); // First chunk with complete tool call and some text after let text = "[get_weather(location='Tokyo')] Here's the forecast:"; let result = parser.parse_incremental(text, &mut state).await.unwrap(); match result { sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => { assert_eq!(tool.function.name, "get_weather"); // Text after tool call should remain in buffer // Note: Current implementation may clear buffer, this behavior needs verification } _ => panic!("Should return ToolComplete"), } } #[tokio::test] async fn test_parse_streaming_multiple_tool_calls() { // Test parsing multiple tool calls in sequence let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let text = "[get_weather(location='Berlin'), search(query='restaurants')]"; // Current implementation may handle this as a single parse let result = parser.parse_incremental(text, &mut state).await.unwrap(); // The parser should handle multiple tools in one bracket pair match result { sglang_router_rs::tool_parser::StreamResult::ToolComplete(_) => { // Expected behavior - parses first tool } _ => { // Also acceptable if it returns Incomplete waiting for more } } } #[tokio::test] async fn test_parse_streaming_opening_bracket_only() { // Test parsing text with only an opening bracket but no closing bracket let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let text = "Let's try this: ["; let result = parser.parse_incremental(text, &mut state).await.unwrap(); match result { sglang_router_rs::tool_parser::StreamResult::Incomplete => { assert!(state.buffer.ends_with("[")); } _ => panic!("Should return Incomplete for partial bracket"), } } #[tokio::test] async fn test_parse_streaming_nested_brackets() { // Test parsing tool calls with nested brackets in arguments let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let text = "[get_weather(location='New York', unit='celsius', data=[1, 2, 3])]"; let result = parser.parse_incremental(text, &mut state).await.unwrap(); match result { sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => { assert_eq!(tool.function.name, "get_weather"); let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap(); assert_eq!(args["location"], "New York"); assert_eq!(args["unit"], "celsius"); assert_eq!(args["data"], json!([1, 2, 3])); } _ => panic!("Should return ToolComplete"), } } #[tokio::test] async fn test_parse_streaming_nested_brackets_dict() { // Test parsing tool calls with nested dictionaries and lists let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let text = r#"[search(query='test', config={'options': [1, 2], 'nested': {'key': 'value'}})]"#; let result = parser.parse_incremental(text, &mut state).await.unwrap(); match result { sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => { assert_eq!(tool.function.name, "search"); let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap(); assert_eq!(args["query"], "test"); assert_eq!(args["config"]["options"], json!([1, 2])); assert_eq!(args["config"]["nested"]["key"], "value"); } _ => panic!("Should return ToolComplete"), } } #[tokio::test] async fn test_parse_streaming_multiple_tools_with_nested_brackets() { // Test parsing multiple tool calls with nested brackets let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let text = "[get_weather(location='Paris', data=[10, 20]), search(query='test', filters=['a', 'b'])]"; let result = parser.parse_incremental(text, &mut state).await.unwrap(); // Should parse both tools successfully match result { sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => { // At least gets the first tool assert_eq!(tool.function.name, "get_weather"); } _ => panic!("Should return ToolComplete"), } } #[tokio::test] async fn test_parse_streaming_partial_nested_brackets() { // Test parsing partial tool calls with nested brackets across chunks let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); // First chunk with nested brackets but incomplete let text1 = "Here's a call: [get_weather(location='Tokyo', data=[1, 2"; let result1 = parser.parse_incremental(text1, &mut state).await.unwrap(); match result1 { sglang_router_rs::tool_parser::StreamResult::Incomplete => { assert!(state .buffer .contains("[get_weather(location='Tokyo', data=[1, 2")); } _ => panic!("First chunk should return Incomplete"), } // Second chunk completing the nested brackets let text2 = ", 3])]"; let result2 = parser.parse_incremental(text2, &mut state).await.unwrap(); match result2 { sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) => { assert_eq!(tool.function.name, "get_weather"); let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap(); assert_eq!(args["location"], "Tokyo"); assert_eq!(args["data"], json!([1, 2, 3])); } _ => panic!("Second chunk should return ToolComplete"), } } #[tokio::test] async fn test_parse_streaming_with_python_start_and_end_token() { // Test parsing a message that starts with <|python_start|> and <|python_end|> across chunks let parser = PythonicParser::new(); let mut state = sglang_router_rs::tool_parser::ParseState::new(); let chunks = vec![ "Here's a call: ", "<|python_", "start|>[get_weather(location=", "'Tokyo', data=[1, 2", ", 3])]<|python_end|>", ]; let mut got_tool = false; for chunk in chunks { let result = parser.parse_incremental(chunk, &mut state).await.unwrap(); if let sglang_router_rs::tool_parser::StreamResult::ToolComplete(tool) = result { assert_eq!(tool.function.name, "get_weather"); let args: serde_json::Value = serde_json::from_str(&tool.function.arguments).unwrap(); assert_eq!(args["location"], "Tokyo"); assert_eq!(args["data"], json!([1, 2, 3])); got_tool = true; } } assert!(got_tool, "Should have parsed the tool call"); } #[tokio::test] async fn test_detect_and_parse_with_python_start_and_end_token() { // Test parsing a message that starts with <|python_start|> and contains a valid tool call let parser = PythonicParser::new(); 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."; let result = parser.parse_complete(text).await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].function.name, "get_weather"); let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap(); assert_eq!(args["location"], "Mars"); assert_eq!(args["unit"], "celsius"); }