char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
acecd5603
char/aliceai-t5-moe
1#include "chat-peg-parser.h" 2#include "chat.h" 3#include "common.h" 4#include "json-schema-to-grammar.h" 5#include "peg-parser.h" 6#include "testing.h" 7#include "peg-parser/simple-tokenize.h" 8 9#include <iostream> 10#include <numeric> 11#include <regex> 12#include <string> 13 14#include "json.h" 15 16using json = common_json ; 17 18static json create_tools (); 19static void test_example_native (testing & t ); 20static void test_example_qwen3_coder (testing & t ); 21static void test_example_qwen3_non_coder (testing & t ); 22static void test_command7_parser_compare (testing & t ); 23static void test_prefix_tool_names (testing & t ); 24static void test_tagged_peg_parser (testing & t ); 25static void test_permute (testing & t ); 26 27int main (int argc ,char * argv []) { 28testing t (std::cout ); 29if (argc >=2 ) { 30t .set_filter (argv [1 ]); 31 } 32 33const char * verbose = getenv ("LLAMA_TEST_VERBOSE" ); 34if (verbose ) { 35t .verbose = std::string (verbose )== "1" ; 36 } 37 38t .test ("native" ,test_example_native ); 39t .test ("qwen3 coder" ,test_example_qwen3_coder ); 40t .test ("qwen3 non-coder" ,test_example_qwen3_non_coder ); 41t .test ("comparison" ,test_command7_parser_compare ); 42t .test ("prefix tool names" ,test_prefix_tool_names ); 43t .test ("tagged peg parser" ,test_tagged_peg_parser ); 44t .test ("permute" ,test_permute ); 45 46return t .summary (); 47} 48 49static json create_tools () { 50json tools = json::array (); 51 52json tool_weather = { 53 {"type" ,"function" }, 54 {"function" , 55 { 56 {"name" ,"get_current_weather" }, 57 {"description" ,"Get the current weather in a given location" }, 58 {"parameters" , 59 { 60 {"type" ,"object" }, 61 {"properties" , 62 { {"location" , 63 { {"type" ,"string" }, {"description" ,"The city and state, e.g. San Francisco, CA" } } }, 64 {"unit" , 65 { {"type" ,"string" }, 66 {"enum" , json::array ({"celsius" ,"fahrenheit" }) }, 67 {"description" , 68"The temperature unit to use. Infer this from the users location." } } } } }, 69 {"required" , json::array ({"location" ,"unit" }) }, 70 } }, 71 } } 72 }; 73tools .push_back (tool_weather ); 74 75json tool_forecast = { 76 {"type" ,"function" }, 77 {"function" , 78 { 79 {"name" ,"get_forecast" }, 80 {"description" ,"Get the weather forecast for a given location" }, 81 {"parameters" , 82 { 83 {"type" ,"object" }, 84 {"properties" , 85 { {"location" , 86 { {"type" ,"string" }, {"description" ,"The city and state, e.g. San Francisco, CA" } } }, 87 {"unit" , 88 { {"type" ,"string" }, 89 {"enum" , json::array ({"celsius" ,"fahrenheit" }) }, 90 {"description" ,"The temperature unit to use. Infer this from the users location." } } }, 91 {"days" , 92 { {"type" ,"integer" }, 93 {"description" ,"Number of days to forecast (1-10)" }, 94 {"minimum" ,1 }, 95 {"maximum" ,10 } } } } }, 96 {"required" , json::array ({"location" ,"unit" }) }, 97 } }, 98 } } 99 }; 100tools .push_back (tool_forecast ); 101 102json tool_search = { 103 {"type" ,"function" }, 104 {"function" , 105 { {"name" ,"search_knowledge_base" }, 106 {"description" ,"Search the internal technical documentation knowledge base." }, 107 {"parameters" , 108 { {"type" ,"object" }, 109 {"properties" , 110 { {"query" , { {"type" ,"string" }, {"description" ,"The search query string." } } }, 111 {"max_results" , 112 { {"type" ,"integer" }, 113 {"description" ,"The maximum number of results to return." }, 114 {"default" ,5 } } }, 115 {"category" , 116 { {"type" ,"string" }, 117 {"enum" , json::array ({"api" ,"troubleshooting" ,"billing" ,"general" }) }, 118 {"description" ,"Filter search by specific category." } } } } }, 119 {"required" , json::array ({"query" ,"category" }) }, 120 {"additionalProperties" , false } } }, 121 {"strict" , true } } } 122 }; 123tools .push_back (tool_search ); 124 125return tools ; 126} 127 128struct tool_argument { 129 std::string name ; 130 std::string type ; 131bool is_required ; 132json schema ; 133}; 134 135struct tool_definition { 136 std::string name ; 137 std::vector < tool_argument > arguments ; 138json schema ; 139}; 140 141// Test fictitious model output that emits arguments as JSON. 142static void test_example_native (testing & t ) { 143struct test_case { 144// Parameters 145 std::string name ; 146json tools ; 147common_chat_tool_choice tool_choice ; 148common_reasoning_format reasoning_format ; 149json json_schema ; 150bool parallel_tool_calls ; 151 std::string generation_prompt ; 152 std::string input ; 153 154// Expect 155 std::string expect_reasoning ; 156 std::string expect_content ; 157 std::vector < common_chat_tool_call > expect_tool_calls ; 158 }; 159 160auto build_parser = [](const test_case & tc ) { 161return build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 162auto reasoning_in_content = (tc .reasoning_format == COMMON_REASONING_FORMAT_NONE ); 163// Always use optional TAG_BASED pattern; generation_prompt is prepended to input 164auto reasoning = p .optional ("<think>" + p .reasoning (p .until ("</think>" ))+ "</think>" + p .space ()); 165 166// tool calling parser 167if (tc .tools .is_array ()&& !tc .tools .empty ()) { 168auto tool_call = 169p .standard_json_tools ("<tool_call>[" ,"]</tool_call>" ,tc .tools ,tc .parallel_tool_calls , 170tc .tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ); 171 172return p .sequence ({ (reasoning_in_content ?p .eps () :reasoning ),p .content (p .until ("<tool_call>" )), 173p .optional (p .space ()+ tool_call ),p .space (),p .end () }); 174 } 175 176// response_format parser 177if (tc .json_schema .is_object ()&& !tc .json_schema .empty ()) { 178return p .sequence ({ (reasoning_in_content ?p .eps () :reasoning ), 179p .content (p .schema (p .json (),"response-output" ,tc .json_schema )),p .space (), 180p .end () }); 181 } 182 183// Content-only parser 184return p .sequence ({ (reasoning_in_content ?p .eps () :reasoning ),p .content (p .rest ()),p .end () }); 185 }); 186 }; 187 188 std::vector < test_case > test_cases = std::vector < test_case > { 189 { 190/* .name = */ "content with reasoning (no generation_prompt)" , 191/* .tools = */ {}, 192/* .tool_choice = */ COMMON_CHAT_TOOL_CHOICE_NONE , 193/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO , 194/* .json_schema = */ {}, 195/* .parallel_tool_calls = */ false, 196/* .generation_prompt = */ "" , 197/* .input = */ ("<think>The user said hello, I must say hello back</think>\nHello" ), 198/* .expect_reasoning = */ "The user said hello, I must say hello back" , 199/* .expect_content = */ "Hello" , 200/* .expect_tool_calls = */ {}, 201 }, 202 { 203/* .name = */ "content without reasoning (no generation_prompt)" , 204/* .tools = */ {}, 205/* .tool_choice = */ COMMON_CHAT_TOOL_CHOICE_NONE , 206/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO , 207/* .json_schema = */ {}, 208/* .parallel_tool_calls = */ false, 209/* .generation_prompt = */ "" , 210/* .input = */ ("Hello" ), 211/* .expect_reasoning = */ "" , 212/* .expect_content = */ "Hello" , 213/* .expect_tool_calls = */ {}, 214 }, 215 { 216/* .name = */ "content with reasoning_format = none (tags appear in content)" , 217/* .tools = */ {}, 218/* .tool_choice = */ COMMON_CHAT_TOOL_CHOICE_NONE , 219/* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE , 220/* .json_schema = */ {}, 221/* .parallel_tool_calls = */ false, 222/* .generation_prompt = */ "" , 223/* .input = */ ("<think>The user said hello, I must say hello back</think>\nHello" ), 224/* .expect_reasoning = */ "" , 225/* .expect_content = */ "<think>The user said hello, I must say hello back</think>\nHello" , 226/* .expect_tool_calls = */ {}, 227 }, 228 { 229/* .name = */ "content with reasoning generation_prompt" , 230/* .tools = */ {}, 231/* .tool_choice = */ COMMON_CHAT_TOOL_CHOICE_NONE , 232/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO , 233/* .json_schema = */ {}, 234/* .parallel_tool_calls = */ false, 235/* .generation_prompt = */ "<think>" , 236/* .input = */ ("The user said hello, I must say hello back</think>\nHello" ), 237/* .expect_reasoning = */ "The user said hello, I must say hello back" , 238/* .expect_content = */ "Hello" , 239/* .expect_tool_calls = */ {}, 240 }, 241 { 242/* .name = */ "content with reasoning generation_prompt and reasoning_format = none" , 243/* .tools = */ {}, 244/* .tool_choice = */ COMMON_CHAT_TOOL_CHOICE_NONE , 245/* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE , 246/* .json_schema = */ {}, 247/* .parallel_tool_calls = */ false, 248/* .generation_prompt = */ "" , 249/* .input = */ ("The user said hello, I must say hello back</think>\nHello" ), 250/* .expect_reasoning = */ "" , 251/* .expect_content = */ "The user said hello, I must say hello back</think>\nHello" , 252/* .expect_tool_calls = */ {}, 253 }, 254 { 255/* .name = */ "content with closed reasoning generation_prompt (empty reasoning discarded)" , 256/* .tools = */ {}, 257/* .tool_choice = */ COMMON_CHAT_TOOL_CHOICE_NONE , 258/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO , 259/* .json_schema = */ {}, 260/* .parallel_tool_calls = */ false, 261/* .generation_prompt = */ "<think></think>" , 262/* .input = */ ("Hello" ), 263/* .expect_reasoning = */ "" , 264/* .expect_content = */ "Hello" , 265/* .expect_tool_calls = */ {}, 266 }, 267 { 268/* .name = */ "tools with reasoning generation_prompt" , 269/* .tools = */ create_tools (), 270/* .tool_choice = */ COMMON_CHAT_TOOL_CHOICE_AUTO , 271/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO , 272/* .json_schema = */ {}, 273/* .parallel_tool_calls = */ false, 274/* .generation_prompt = */ "<think>" , 275/* .input = */ 276 ("I must get the weather in New York</think>\n" 277"<tool_call>[" 278R"({"name": "get_current_weather", "arguments": {"location": "New York City, NY", "unit": "fahrenheit"}})" 279"]</tool_call>" ), 280/* .expect_reasoning = */ "I must get the weather in New York" , 281/* .expect_content = */ "" , 282/* .expect_tool_calls = */ 283 { { 284/* .name = */ "get_current_weather" , 285/* .arguments = */ R"({"location": "New York City, NY", "unit": "fahrenheit"})" , 286/* .id = */ "" , 287 } }, 288 }, 289 { 290/* .name = */ "parallel tools with reasoning generation_prompt" , 291/* .tools = */ create_tools (), 292/* .tool_choice = */ COMMON_CHAT_TOOL_CHOICE_AUTO , 293/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO , 294/* .json_schema = */ {}, 295/* .parallel_tool_calls = */ true, 296/* .generation_prompt = */ "<think>" , 297/* .input = */ 298 ("I must get the weather in New York and San Francisco and a 3 day forecast of each.</think>\nLet me " 299"search that for you." 300"<tool_call>[" 301R"({"name": "get_current_weather", "arguments": {"location": "New York City, NY", "unit": "fahrenheit"}})" 302", " 303R"({"name": "get_current_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}})" 304", " 305R"({"name": "get_forecast", "arguments": {"location": "New York City, NY", "unit": "fahrenheit", "days": 3}})" 306", " 307R"({"name": "get_forecast", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit", "days": 3}})" 308"]</tool_call>" ), 309/* .expect_reasoning = */ 310"I must get the weather in New York and San Francisco and a 3 day forecast of each." ,/* .expect_content = */ "Let me search that for you." , 311/* .expect_tool_calls = */ 312 { { 313/* .name = */ "get_current_weather" , 314/* .arguments = */ R"({"location": "New York City, NY", "unit": "fahrenheit"})" , 315/* .id = */ "" , 316 }, 317 { 318/* .name = */ "get_current_weather" , 319/* .arguments = */ R"({"location": "San Francisco, CA", "unit": "fahrenheit"})" , 320/* .id = */ "" , 321 }, 322 { 323/* .name = */ "get_forecast" , 324/* .arguments = */ R"({"location": "New York City, NY", "unit": "fahrenheit", "days": 3})" , 325/* .id = */ "" , 326 }, 327 { 328/* .name = */ "get_forecast" , 329/* .arguments = */ R"({"location": "San Francisco, CA", "unit": "fahrenheit", "days": 3})" , 330/* .id = */ "" , 331 } }, 332 }, 333 { 334/* .name = */ "response_format with reasoning generation_prompt" , 335/* .tools = */ {}, 336/* .tool_choice = */ COMMON_CHAT_TOOL_CHOICE_NONE , 337/* .reasoning_format = */ COMMON_REASONING_FORMAT_AUTO , 338/* .json_schema = */ 339 { {"type" ,"object" }, 340 {"properties" , 341 { {"invoice_number" , { {"type" ,"string" } } }, 342 {"amount" , { {"type" ,"number" } } }, 343 {"due_date" , { {"type" ,"string" } } } } }, 344 {"required" , json::array ({"invoice_number" ,"amount" ,"due_date" }) } }, 345/* .parallel_tool_calls = */ false, 346/* .generation_prompt = */ "<think>" , 347/* .input = */ 348 ("I must produce the invoice in the requested format</think>\n" 349R"({"invoice_number": "INV-2025-001", "amount": 1250.50, "due_date": "2025-12-31"})" ), 350/* .expect_reasoning = */ "I must produce the invoice in the requested format" , 351/* .expect_content = */ 352R"({"invoice_number": "INV-2025-001", "amount": 1250.50, "due_date": "2025-12-31"})" ,/* .expect_tool_calls = */ {}, 353 }, 354 }; 355 356for (const auto & tc :test_cases ) { 357t .test (tc .name , [& ](testing & t ) { 358auto parser = build_parser (tc ); 359auto lazy = !tc .tools .empty ()&& tc .tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED ; 360auto grammar = build_grammar ([& ](const common_grammar_builder & builder ) { 361parser .build_grammar (builder ,lazy ); 362 }); 363 364t .log ("Grammar:" ); 365for (const auto & line :string_split (grammar ,"\n" )) { 366t .log (line ); 367 } 368 369 std::string effective_input = tc .generation_prompt + tc .input ; 370common_peg_parse_context ctx (effective_input ); 371auto result = parser .parse (ctx ); 372 373t .assert_true ("success" ,result .success ()); 374 375common_chat_msg msg ; 376auto mapper = common_chat_peg_mapper (msg ); 377mapper .from_ast (ctx .ast ,result ); 378 379t .assert_equal ("content equal" ,tc .expect_content ,msg .content ); 380t .assert_equal ("reasoning equal" ,tc .expect_reasoning ,msg .reasoning_content ); 381t .assert_equal ("number of tool calls" ,tc .expect_tool_calls .size (),msg .tool_calls .size ()); 382for (auto i = 0u ;i < std::min (tc .expect_tool_calls .size (),msg .tool_calls .size ());i ++ ) { 383t .assert_equal ("tool name" ,tc .expect_tool_calls [i ].name ,msg .tool_calls [i ].name ); 384t .assert_equal ("tool args" ,tc .expect_tool_calls [i ].arguments ,msg .tool_calls [i ].arguments ); 385 } 386 }); 387 } 388} 389 390static void test_example_qwen3_coder (testing & t ) { 391auto tools = create_tools (); 392auto parser = build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 393auto content = p .rule ("content" ,p .content (p .until ("<tool_call>" ))); 394 395 std::vector < common_peg_parser > tool_parsers ; 396for (const auto & def :tools ) { 397auto function = def .at ("function" ); 398 std::string name = function .at ("name" ); 399auto parameters = function .at ("parameters" ); 400auto properties = parameters .at ("properties" ); 401 402 std::set < std::string > required_properties ; 403if (function .contains ("required" )) { 404required_properties = function .at ("required" ).get < std::set < std::string >> (); 405 } 406 407 std::vector < common_peg_parser > arg_parsers ; 408for (const auto & [param_name ,param_schema ] :properties .items ()) { 409bool is_required = required_properties .find (param_name )!= required_properties .end (); 410auto type = param_schema .value ("type" ,"object" ); 411 412auto arg = p .tool_arg ( 413p .sequence ({p .tool_arg_open ("<parameter=" + p .tool_arg_name (p .literal (param_name ))+ ">" ), 414 (type == "string" ? 415p .tool_arg_string_value (p .schema ( 416p .until_one_of ({"</parameter>\n<parameter=" ,"</parameter>\n</function>" }), 417"tool-" + name + "-arg-" + param_name + "-schema" ,param_schema , true)) : 418p .tool_arg_json_value (p .schema ( 419p .json (),"tool-" + name + "-arg-" + param_name + "-schema" ,param_schema ))), 420p .tool_arg_close ("</parameter>\n" + 421p .peek (p .literal ("<parameter=" ) |p .literal ("</function>" ))) })); 422 423arg_parsers .push_back (is_required ?p .rule ("tool-" + name + "-arg-" + param_name ,arg ) : 424p .optional (p .rule ("tool-" + name + "-arg-" + param_name ,arg ))); 425 } 426 427tool_parsers .push_back (p .rule ("tool-" + name ,p .tool_open ("<function=" + p .tool_name (p .literal (name ))+ ">" ) 428 <<p .sequence (arg_parsers ) 429 <<p .tool_close (p .literal ("</function>" )))); 430 }; 431 432auto tool_call = p .trigger_rule ("tool-call" ,"<tool_call>" <<p .choice (tool_parsers ) <<"</tool_call>" ); 433 434return content + p .zero_or_more (p .space ()+ tool_call )+ p .end (); 435 }); 436 437auto grammar = build_grammar ([& ](const common_grammar_builder & builder ) { 438parser .build_grammar (builder ); 439 }); 440 441t .log ("Grammar:" ); 442for (const auto & line :string_split (grammar ,"\n" )) { 443t .log (line ); 444 } 445 446t .test ("incremental parsing" , [& ](testing & t ) { 447 std::string input = 448"Let me search the knowledge base for cat pictures." 449"<tool_call>\n" 450"<function=search_knowledge_base>\n" 451"<parameter=query>cat pictures</parameter>\n" 452"<parameter=category>general</parameter>\n" 453"</function>\n" 454"</tool_call>" ; 455 456 std::vector < std::string > tokens = simple_tokenize (input ); 457 458common_chat_msg prev ; 459for (auto it = tokens .begin ();it != tokens .end ();it ++ ) { 460 std::string in = std::accumulate (tokens .begin (),it + 1 , std::string ()); 461 462common_peg_parse_context ctx (in , (it + 1 < tokens .end ()) ?COMMON_PEG_PARSE_FLAG_LENIENT :COMMON_PEG_PARSE_FLAG_NONE ); 463 464auto result = parser .parse (ctx ); 465if (!t .assert_equal ("not fail" , false,result .fail ())) { 466t .log (in .substr (0 ,result .end )+ "[failed->]" + in .substr (result .end )); 467 } 468 469common_chat_msg msg ; 470auto mapper = common_chat_peg_mapper (msg ); 471mapper .from_ast (ctx .ast ,result ); 472 473//t.log("Input: " + input); 474t .log ("===========================================" ); 475t .log ("Iteration " + std::to_string (in .size ())); 476t .log ("Reasoning: " + msg .reasoning_content ); 477t .log ("Content : " + msg .content ); 478for (const auto & tc :msg .tool_calls ) { 479t .log ("Tool name: " + tc .name ); 480t .log ("Tool args: " + tc .arguments ); 481 } 482 483try { 484// This shouldn't emit any runtime errors 485auto diffs = common_chat_msg_diff::compute_diffs (prev ,msg ); 486 }catch (const std::exception & e ) { 487t .log (in .substr (0 ,result .end )+ "[failed->]" + in .substr (result .end )); 488t .assert_true (std::string ("failed with " )+ e .what (), false); 489 } 490 491prev = msg ; 492 } 493 }); 494} 495 496static void test_example_qwen3_non_coder (testing & t ) { 497auto tools = create_tools (); 498auto parser = build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 499// tool calling parser using standard JSON format 500auto tool_call = p .standard_json_tools ("<tool_call>" ,"</tool_call>" ,tools , true, false); 501 502return p .sequence ({p .content (p .until ("<tool_call>" )),p .optional (p .space ()+ tool_call ),p .end () }); 503 }); 504 505auto grammar = build_grammar ([& ](const common_grammar_builder & builder ) { 506parser .build_grammar (builder ); 507 }); 508 509t .log ("Grammar:" ); 510for (const auto & line :string_split (grammar ,"\n" )) { 511t .log (line ); 512 } 513 514t .test ("tool call parsing" , [& ](testing & t ) { 515 std::string input = 516"I need to get the weather.\n" 517"<tool_call>" 518"{\"name\": \"get_current_weather\", \"arguments\": {\"location\": \"New York City, NY\", \"unit\": " 519"\"fahrenheit\"}}" 520"</tool_call>" ; 521 522common_peg_parse_context ctx (input ); 523auto result = parser .parse (ctx ); 524 525t .assert_true ("success" ,result .success ()); 526 527common_chat_msg msg ; 528auto mapper = common_chat_peg_mapper (msg ); 529mapper .from_ast (ctx .ast ,result ); 530 531t .assert_equal ("content" ,"I need to get the weather.\n" ,msg .content ); 532t .assert_equal ("reasoning" ,"" ,msg .reasoning_content ); 533t .assert_equal ("tool calls count" ,1u ,msg .tool_calls .size ()); 534if (!msg .tool_calls .empty ()) { 535t .assert_equal ("tool name" ,"get_current_weather" ,msg .tool_calls [0 ].name ); 536t .assert_equal ("tool args" ,"{\"location\": \"New York City, NY\", \"unit\": \"fahrenheit\"}" , 537msg .tool_calls [0 ].arguments ); 538 } 539 }); 540 541t .test ("incremental parsing" , [& ](testing & t ) { 542 std::string input = 543"I need to get the weather.\n" 544"<tool_call>" 545"{\"name\": \"get_current_weather\", \"arguments\": {\"location\": \"New York City, NY\", \"unit\": " 546"\"fahrenheit\"}}" 547"</tool_call>" ; 548 549 std::vector < std::string > tokens = simple_tokenize (input ); 550 551common_chat_msg prev ; 552for (auto it = tokens .begin ();it != tokens .end ();it ++ ) { 553 std::string in = std::accumulate (tokens .begin (),it + 1 , std::string ()); 554 555common_peg_parse_context ctx (in , (it + 1 < tokens .end ()) ?COMMON_PEG_PARSE_FLAG_LENIENT :COMMON_PEG_PARSE_FLAG_NONE ); 556 557auto result = parser .parse (ctx ); 558if (!t .assert_equal ("not fail" , false,result .fail ())) { 559t .log (in .substr (0 ,result .end )+ "[failed->]" + in .substr (result .end )); 560 } 561 562common_chat_msg msg ; 563auto mapper = common_chat_peg_mapper (msg ); 564mapper .from_ast (ctx .ast ,result ); 565 566//t.log("Input: " + input); 567t .log ("===========================================" ); 568t .log ("Iteration " + std::to_string (in .size ())); 569t .log ("Reasoning: " + msg .reasoning_content ); 570t .log ("Content : " + msg .content ); 571for (const auto & tc :msg .tool_calls ) { 572t .log ("Tool name: " + tc .name ); 573t .log ("Tool args: " + tc .arguments ); 574 } 575 576try { 577// This shouldn't emit any runtime errors 578auto diffs = common_chat_msg_diff::compute_diffs (prev ,msg ); 579 }catch (const std::exception & e ) { 580t .log (in .substr (0 ,result .end )+ "[failed->]" + in .substr (result .end )); 581t .assert_true (std::string ("failed with " )+ e .what (), false); 582 } 583 584prev = msg ; 585 } 586 }); 587} 588 589void test_command7_parser_compare (testing & t ) { 590auto parser = build_chat_peg_parser ([](common_chat_peg_builder & p ) { 591auto thinking = 592p .reasoning_block ("<|START_THINKING|>" <<p .reasoning (p .until ("<|END_THINKING|>" )) <<"<|END_THINKING|>" ); 593 594auto response = "<|START_RESPONSE|>" <<p .content (p .until ("<|END_RESPONSE|>" )) <<"<|END_RESPONSE|>" ; 595 596auto tool_call_id = p .atomic ("\"tool_call_id\"" << (":" << ("\"" + p .tool_id (p .string_content ('"' ))+ "\"" ))); 597auto tool_call_name = 598p .atomic ("\"tool_name\"" << (":" << ("\"" + p .tool_name (p .string_content ('"' ))+ "\"" ))); 599auto tool_call_args = "\"parameters\"" << (":" <<p .tool_args (p .json ())); 600 601auto tool_call_fields = p .rule ("tool-call-fields" ,tool_call_id |tool_call_name |tool_call_args ); 602auto tool_call = 603p .rule ("tool-call" ,p .tool (p .tool_open (p .literal ("{" )) 604 <<tool_call_fields <<p .zero_or_more (p .literal ("," ) <<tool_call_fields ) 605 <<p .tool_close (p .literal ("}" )))); 606 607auto tool_calls = p .rule ( 608"tool-calls" ,"<|START_ACTION|>" << ("[" <<tool_call <<p .zero_or_more (p .literal ("," ) <<tool_call ) <<"]" ) 609 <<"<|END_ACTION|>" ); 610 611return p .optional (thinking ) << (tool_calls |response )+ p .end (); 612 }); 613 614auto test_current = [& ](const common_peg_arena & p ,const std::string & input ,bool is_partial , 615bool print_results ) { 616common_peg_parse_context ctx (input ,is_partial ?COMMON_PEG_PARSE_FLAG_LENIENT :COMMON_PEG_PARSE_FLAG_NONE ); 617auto result = p .parse (ctx ); 618 619common_chat_msg msg ; 620auto mapper = common_chat_peg_mapper (msg ); 621mapper .from_ast (ctx .ast ,result ); 622 623if (print_results ) { 624 std::cout <<"== Parsed (new) ==\n" ; 625 std::cout <<"=== Reasoning ===\n" ; 626 std::cout <<msg .reasoning_content <<"\n" ; 627 std::cout <<"\n\n=== Content ===\n" ; 628 std::cout <<msg .content <<"\n" ; 629 std::cout <<"\n\n=== Tool Calls ===\n" ; 630for (const auto & tc :msg .tool_calls ) { 631 std::cout <<"id: " <<tc .id <<"\n" ; 632 std::cout <<"name: " <<tc .name <<"\n" ; 633 std::cout <<"args: " <<tc .arguments <<"\n" ; 634 } 635 } 636 }; 637 638 std::string reasoning = 639"To plan an effective trip to Japan that includes both historical sites and modern attractions within a " 640"budget of $4000 for a two-week stay, we need to:\n\n" 641"1. Identify key historical sites and modern attractions in Japan.\n" 642"2. Find affordable accommodation options that provide a balance between comfort and cost.\n" 643"3. Determine the best modes of transportation for getting around Japan.\n" 644"4. Create a day-by-day itinerary that ensures the user gets to see a variety of attractions without " 645"overspending.\n" 646"5. Provide a detailed cost breakdown that includes accommodation, transportation, meals, and entry fees " 647"to attractions." ; 648 649 std::vector < std::tuple < std::string , std::string ,common_json >> tool_calls = { 650 {"call_0" ,"plan_trip" , common_json::parse (R"({ 651"destination": "Japan", 652"duration": 14, 653"budget": 4000, 654"interests": ["historical sites", "modern attractions"], 655"accommodation_preferences": "affordable", 656"transportation_preferences": "efficient", 657"meal_preferences": "local cuisine" 658})" ) } 659 }; 660 661 std::vector < std::string > tokens ; 662 663// Build tokens 664if (!reasoning .empty ()) { 665auto tokenized = simple_tokenize (reasoning ); 666tokens .emplace_back ("<|START_THINKING|>" ); 667tokens .insert (tokens .end (),tokenized .begin (),tokenized .end ()); 668tokens .emplace_back ("<|END_THINKING|>" ); 669 } 670 671if (!tool_calls .empty ()) { 672tokens .emplace_back ("<|START_ACTION|>" ); 673 674auto json = common_json::array (); 675for (const auto & tc :tool_calls ) { 676auto tc_json = common_json::object (); 677tc_json ["tool_call_id" ]= std::get < 0 > (tc ); 678tc_json ["tool_name" ]= std::get < 1 > (tc ); 679tc_json ["parameters" ]= std::get < 2 > (tc ); 680json .push_back (tc_json ); 681 } 682 683auto tokenized = simple_tokenize (json .dump (-1 )); 684tokens .insert (tokens .end (),tokenized .begin (),tokenized .end ()); 685 686tokens .emplace_back ("<|END_ACTION|>" ); 687 } 688 689 std::string input = std::accumulate (tokens .begin (),tokens .end (), std::string ()); 690 691t .test ("current_parse" , [& ](testing & /* t */ ) {test_current (parser ,input , false, false); }); 692t .bench ("current_parse_benchmark complete" , [& ]() {test_current (parser ,input , false, false); },100 ); 693t .bench ( 694"current_parse_benchmark incremental" , 695 [& ]() { 696 std::string in ; 697for (auto i = 0u ;i < tokens .size ();i ++ ) { 698in += tokens [i ]; 699test_current (parser ,in ,i + 1 < tokens .size (), false); 700 } 701 }, 70220 ); 703} 704 705// Test that tool names that are proper prefixes of other tool names don't cause 706// premature matching during incremental parsing. 707// For example, "special_function" should not match when parsing "special_function_with_opt". 708static void test_prefix_tool_names (testing & t ) { 709// Create tools where one name is a proper prefix of another 710json tools = json::array (); 711 712json tool_short = { 713 {"type" ,"function" }, 714 {"function" , 715 { 716 {"name" ,"special_function" }, 717 {"description" ,"A special function" }, 718 {"parameters" , 719 { 720 {"type" ,"object" }, 721 {"properties" , 722 { 723 {"arg1" , { {"type" ,"integer" } } }, 724 } }, 725 {"required" , json::array ({"arg1" }) }, 726 } }, 727 } } 728 }; 729tools .push_back (tool_short ); 730 731json tool_long = { 732 {"type" ,"function" }, 733 {"function" , 734 { 735 {"name" ,"special_function_with_opt" }, 736 {"description" ,"A special function with optional params" }, 737 {"parameters" , 738 { 739 {"type" ,"object" }, 740 {"properties" , 741 { 742 {"arg1" , { {"type" ,"integer" } } }, 743 {"arg2" , { {"type" ,"integer" } } }, 744 } }, 745 {"required" , json::array ({"arg1" }) }, 746 } }, 747 } } 748 }; 749tools .push_back (tool_long ); 750 751// Use standard_constructed_tools which had the prefix matching bug 752 std::map < std::string , std::string > markers = { 753 {"tool_call_start_marker" ,"<tool_call>" }, 754 {"tool_call_end_marker" ,"</tool_call>" }, 755 {"function_opener" ,"<function=" }, 756 {"function_closer" ,"</function>" }, 757 {"function_name_suffix" ,">" }, 758 {"parameter_key_prefix" ,"<param=" }, 759 {"parameter_key_suffix" ,">" }, 760 {"parameter_closer" ,"</param>" }, 761 }; 762 763auto parser = build_chat_peg_parser ([& ](common_chat_peg_builder & p ) { 764auto content = p .rule ("content" ,p .content (p .until ("<tool_call>" ))); 765auto tool_call = p .standard_constructed_tools (markers ,tools , false, false); 766return content + p .zero_or_more (p .space ()+ tool_call )+ p .end (); 767 }); 768 769// Test parsing the long tool name - this should NOT trigger the short tool name 770t .test ("parse long tool name" , [& ](testing & t ) { 771 std::string input = 772"Let me call the function." 773"<tool_call>" 774"<function=special_function_with_opt>" 775"<param=arg1>42</param>" 776"</function>" 777"</tool_call>" ; 778 779common_peg_parse_context ctx (input ); 780auto result = parser .parse (ctx ); 781 782t .assert_true ("success" ,result .success ()); 783 784common_chat_msg msg ; 785auto mapper = common_chat_peg_mapper (msg ); 786mapper .from_ast (ctx .ast ,result ); 787 788t .assert_equal ("content" ,"Let me call the function." ,msg .content ); 789t .assert_equal ("tool calls count" ,1u ,msg .tool_calls .size ()); 790if (!msg .tool_calls .empty ()) { 791t .assert_equal ("tool name" ,"special_function_with_opt" ,msg .tool_calls [0 ].name ); 792 } 793 }); 794 795// Test incremental parsing - the key test case 796// This ensures that when incrementally parsing "special_function_with_opt", 797// we don't prematurely emit "special_function" as a tool call 798t .test ("incremental parse long tool name" , [& ](testing & t ) { 799 std::string input = 800"Let me call the function." 801"<tool_call>" 802"<function=special_function_with_opt>" 803"<param=arg1>42</param>" 804"</function>" 805"</tool_call>" ; 806 807 std::vector < std::string > tokens = simple_tokenize (input ); 808 809common_chat_msg prev ; 810for (auto it = tokens .begin ();it != tokens .end ();it ++ ) { 811 std::string in = std::accumulate (tokens .begin (),it + 1 , std::string ()); 812 813common_peg_parse_context ctx (in , (it + 1 < tokens .end ()) ?COMMON_PEG_PARSE_FLAG_LENIENT :COMMON_PEG_PARSE_FLAG_NONE ); 814auto result = parser .parse (ctx ); 815 816if (!t .assert_equal ("not fail" , false,result .fail ())) { 817t .log (in .substr (0 ,result .end )+ "[failed->]" + in .substr (result .end )); 818return ; 819 } 820 821common_chat_msg msg ; 822auto mapper = common_chat_peg_mapper (msg ); 823mapper .from_ast (ctx .ast ,result ); 824 825// The critical check: during incremental parsing, we should never 826// see "special_function" as the tool name when parsing "special_function_with_opt" 827for (const auto & tc :msg .tool_calls ) { 828if (!t .assert_equal ("tool name should not be short prefix" , false, 829tc .name == "special_function" )) { 830t .log ("Premature tool name match at input: " + in ); 831return ; 832 } 833 } 834 835try { 836auto diffs = common_chat_msg_diff::compute_diffs (prev ,msg ); 837 }catch (const std::exception & e ) { 838t .log (in .substr (0 ,result .end )+ "[failed->]" + in .substr (result .end )); 839t .assert_true (std::string ("diff failed with " )+ e .what (), false); 840return ; 841 } 842 843prev = msg ; 844 } 845 846// Final check: the complete parse should have the correct tool name 847t .assert_equal ("final tool calls count" ,1u ,prev .tool_calls .size ()); 848if (!prev .tool_calls .empty ()) { 849t .assert_equal ("final tool name" ,"special_function_with_opt" ,prev .tool_calls [0 ].name ); 850 } 851 }); 852 853// Test parsing the short tool name still works 854t .test ("parse short tool name" , [& ](testing & t ) { 855 std::string input = 856"Let me call the function." 857"<tool_call>" 858"<function=special_function>" 859"<param=arg1>42</param>" 860"</function>" 861"</tool_call>" ; 862 863common_peg_parse_context ctx (input ); 864auto result = parser .parse (ctx ); 865 866t .assert_true ("success" ,result .success ()); 867 868common_chat_msg msg ; 869auto mapper = common_chat_peg_mapper (msg ); 870mapper .from_ast (ctx .ast ,result ); 871 872t .assert_equal ("content" ,"Let me call the function." ,msg .content ); 873t .assert_equal ("tool calls count" ,1u ,msg .tool_calls .size ()); 874if (!msg .tool_calls .empty ()) { 875t .assert_equal ("tool name" ,"special_function" ,msg .tool_calls [0 ].name ); 876 } 877 }); 878} 879 880static void test_tagged_peg_parser (testing & t ) { 881t .test ("basic tag extraction" , [& ](testing & t ) { 882auto parser = build_tagged_peg_parser ([](common_peg_parser_builder & p ) { 883return p .tag ("greeting" ,p .until (" " ))+ " " + p .tag ("name" ,p .rest ())+ p .end (); 884 }); 885 886auto result = parser .parse_and_extract ("Hello World" ); 887t .assert_true ("success" ,result .result .success ()); 888t .assert_equal ("greeting tag" ,"Hello" ,result .tags .at ("greeting" )); 889t .assert_equal ("name tag" ,"World" ,result .tags .at ("name" )); 890 }); 891 892t .test ("duplicate tags overwrite" , [& ](testing & t ) { 893auto parser = build_tagged_peg_parser ([](common_peg_parser_builder & p ) { 894return p .tag ("item" ,p .until ("," ))+ "," + p .tag ("item" ,p .rest ())+ p .end (); 895 }); 896 897auto result = parser .parse_and_extract ("first,second" ); 898t .assert_true ("success" ,result .result .success ()); 899t .assert_equal ("item tag" ,"second" ,result .tags .at ("item" )); 900 }); 901 902t .test ("no tags extracted" , [& ](testing & t ) { 903auto parser = build_tagged_peg_parser ([](common_peg_parser_builder & p ) { 904return p .rest ()+ p .end (); 905 }); 906 907auto result = parser .parse_and_extract ("Hello" ); 908t .assert_true ("success" ,result .result .success ()); 909t .assert_equal ("empty tags" ,0u ,result .tags .size ()); 910 }); 911 912t .test ("structured extraction" , [& ](testing & t ) { 913auto parser = build_tagged_peg_parser ([](common_peg_parser_builder & p ) { 914auto header = p .tag ("header" ,p .until ("\n" )); 915auto body = p .tag ("body" ,p .rest ()); 916return header + "\n" + body + p .end (); 917 }); 918 919auto result = parser .parse_and_extract ("Title\nBody content here" ); 920t .assert_true ("success" ,result .result .success ()); 921t .assert_equal ("header" ,"Title" ,result .tags .at ("header" )); 922t .assert_equal ("body" ,"Body content here" ,result .tags .at ("body" )); 923 }); 924 925t .test ("partial parse" , [& ](testing & t ) { 926auto parser = build_tagged_peg_parser ([](common_peg_parser_builder & p ) { 927return p .tag ("prefix" ,p .until (":" ))+ ":" + p .tag ("value" ,p .rest ())+ p .end (); 928 }); 929 930auto result = parser .parse_and_extract ("key:val" ,COMMON_PEG_PARSE_FLAG_LENIENT ); 931t .assert_true ("not fail" , !result .result .fail ()); 932t .assert_equal ("prefix tag" ,"key" ,result .tags .at ("prefix" )); 933t .assert_equal ("value tag" ,"val" ,result .tags .at ("value" )); 934 }); 935 936t .test ("find in the middle" , [& ](testing & t ) { 937auto parser = build_tagged_peg_parser ([](common_peg_parser_builder & p ) { 938return p .choice ({p .literal ("{" ),p .literal (":" ) })+ p .space ()+ p .literal ("\"" )+ p .atomic (p .literal ("fun_name" )); 939 }); 940 941 std::string tpl = "This is a very long jinja template string. We have tools. We will try to call them now: <tool_call>{ \"fun_name\" : { \"arg\" : 1 }</tool_call>" ; 942auto result = parser .parse_anywhere_and_extract (tpl ); 943t .assert_true ("success" ,result .result .success ()); 944 }); 945 946t .test ("fail find in the middle" , [& ](testing & t ) { 947auto parser = build_tagged_peg_parser ([](common_peg_parser_builder & p ) { 948return p .choice ({p .literal ("{" ),p .literal (":" ) })+ p .space ()+ p .literal ("\"" )+ p .atomic (p .literal ("fun_name" )); 949 }); 950 951 std::string tpl = "This is a very long jinja template string. We have tools. We will try to call them now: <tool_call><fun=fun_name><arg name=arg>1</arg></tool_call>" ; 952auto result = parser .parse_anywhere_and_extract (tpl ); 953t .assert_true ("failure" ,result .result .fail ()); 954 }); 955 956t .test ("find function tag with name" , [& ](testing & t ) { 957 std::string haystack = "\n<tool_call>\n<function=foofoo>\n<parameter=first>\nXXXX\n</parameter>\n<parameter=second>\nYYYY\n</parameter>\n</function>\n</tool_call>\n" ; 958auto parser = build_tagged_peg_parser ([](common_peg_parser_builder & p ) { 959 std::string needle = "foofoo" ; 960return p .tag ("fun_marker" ,p .choice ({ 961p .tag ("fun_pre" ,p .literal ("<" )+ p .until_one_of ({">" ,needle }))+ p .literal (needle )+ 962p .tag ("fun_post" ,p .negate (p .space ()+ p .literal ("<" ))+ p .until (">" )+ p .literal (">" ))+ p .space (), 963p .tag ("fun_pre" ,p .literal ("[" )+ p .until_one_of ({"]" ,needle }))+ p .literal (needle )+ 964p .tag ("fun_post" ,p .negate (p .space ()+ p .literal ("[" )+ p .until ("]" )+ p .literal ("]" ))+ p .space ()) })); 965 }); 966auto result = parser .parse_anywhere_and_extract (haystack ); 967t .assert_true ("success" ,result .result .success ()); 968t .assert_equal ("fun_pre should be '<function='" ,"<function=" ,result .tags ["fun_pre" ]); 969t .assert_equal ("fun_post should be '>'" ,">" ,result .tags ["fun_post" ]); 970 }); 971} 972 973static void test_permute (testing & t ) { 974auto accepts = [](const common_peg_arena & parser ,const std::string & input ) { 975common_peg_parse_context ctx (input ); 976return parser .parse (ctx ).success (); 977 }; 978 979auto gbnf_of = [](const common_peg_arena & parser ) { 980return build_grammar ([& ](const common_grammar_builder & builder ) {parser .build_grammar (builder ); }); 981 }; 982 983auto assert_gbnf_equal = [](testing & t ,const std::string & expected ,const std::string & actual ) { 984static const std::regex leading_ws_re = std::regex (R"((^|\n)\s+)" ); 985t .assert_equal ("gbnf are equal" , std::regex_replace (expected ,leading_ws_re ,"$1" ),actual ); 986 }; 987 988auto count_rules = [](const std::string & gbnf ,const std::string & prefix ) { 989size_t count = 0 ; 990for (const auto & line :string_split < std::string > (gbnf ,'\n' )) { 991if (line .rfind (prefix ,0 )== 0 ) { 992count ++ ; 993 } 994 } 995return count ; 996 }; 997 998t .test ("accepts every ordering" , [& ](testing & t ) { 999auto parser = build_chat_peg_parser ([](common_chat_peg_builder & p ) { 1000return p .permute ("abc" , {p .literal ("a" ),p .literal ("b" ),p .literal ("c" ) })+ p .end (); 1001 }); 1002 1003for (const std::string input : {"abc" ,"acb" ,"bac" ,"bca" ,"cab" ,"cba" }) { 1004t .assert_true ("accepts " + input ,accepts (parser ,input )); 1005 } 1006 }); 1007 1008t .test ("single element" , [& ](testing & t ) { 1009auto parser = build_chat_peg_parser ([](common_chat_peg_builder & p ) { 1010return p .permute ("a" , {p .literal ("a" ) })+ p .end (); 1011 }); 1012 1013t .assert_true ("accepts a" ,accepts (parser ,"a" )); 1014t .assert_true ("rejects aa" , !accepts (parser ,"aa" )); 1015 }); 1016 1017t .test ("grammar left-factorizes shared tails" , [& ](testing & t ) { 1018auto parser = build_chat_peg_parser ([](common_chat_peg_builder & p ) { 1019return p .permute ("abc" , {p .literal ("a" ),p .literal ("b" ),p .literal ("c" ) })+ p .end (); 1020 }); 1021 1022// Every rule is one remaining subset, keyed by bitmask: abc-3 is {a,b}, abc-7 is {a,b,c}. 1023// Each subset is emitted once and shared by every branch that leads into it. 1024assert_gbnf_equal (t ,R"""( 1025abc-1 ::= "a" 1026abc-2 ::= "b" 1027abc-3 ::= "a" abc-2 | "b" abc-1 1028abc-4 ::= "c" 1029abc-5 ::= "a" abc-4 | "c" abc-1 1030abc-6 ::= "b" abc-4 | "c" abc-2 1031abc-7 ::= "a" abc-6 | "b" abc-5 | "c" abc-3 1032root ::= abc-7 1033space ::= | " " | "\n"{1,2} [ \t]{0,20} 1034)""" ,gbnf_of (parser )); 1035 }); 1036 1037t .test ("grammar emits one rule per remaining subset" , [& ](testing & t ) { 1038auto parser = build_chat_peg_parser ([](common_chat_peg_builder & p ) { 1039return p .permute ("abcd" , {p .literal ("a" ),p .literal ("b" ),p .literal ("c" ),p .literal ("d" ) })+ p .end (); 1040 }); 1041 1042// 2^4 - 1 non-empty subsets, one rule each - not the 4! = 24 orderings. 1043t .assert_equal ("permute rule count" ,15u ,count_rules (gbnf_of (parser ),"abcd-" )); 1044 }); 1045 1046t .test ("grammar emits no rules for a single element" , [& ](testing & t ) { 1047auto parser = build_chat_peg_parser ([](common_chat_peg_builder & p ) { 1048return p .permute ("a" , {p .literal ("a" ) })+ p .end (); 1049 }); 1050 1051assert_gbnf_equal (t ,R"""( 1052root ::= "a" 1053space ::= | " " | "\n"{1,2} [ \t]{0,20} 1054)""" ,gbnf_of (parser )); 1055 }); 1056 1057t .test ("grammar falls back to the given order when too large" , [& ](testing & t ) { 1058auto parser = build_chat_peg_parser ([](common_chat_peg_builder & p ) { 1059 std::vector < common_peg_parser > parsers ; 1060for (size_t i = 0 ;i <=COMMON_CHAT_MAX_PERMUTE ;i ++ ) { 1061parsers .push_back (p .literal (std::string (1 , (char ) ('a' + i )))); 1062 } 1063return p .permute ("big" ,parsers )+ p .end (); 1064 }); 1065 1066assert_gbnf_equal (t ,R"""( 1067root ::= "a" "b" "c" "d" "e" "f" "g" 1068space ::= | " " | "\n"{1,2} [ \t]{0,20} 1069)""" ,gbnf_of (parser )); 1070 }); 1071}