Brunobkr/llama.cpp_AlgMor24_github
ΩFFFΣLLIa • llama.cpp • AlgMor24 ██████╗ ███████╗███████╗███████╗██╗ ██╗ ██╗ █████╗ ██╔═══██╗██╔════╝██╔════╝██╔════╝██║ ██║ ██║██╔══██╗ ██║ ██║█████╗ █████╗ █████╗ ██║ ██║ ██║███████║ ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██║ ██║ ██║██╔══██║ ╚██████╔╝██║ ██║ ███████╗███████╗███████╗██║██║ ██║ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚══════╝╚═╝╚═╝ ╚═╝ High-Performance LLM / VLM Inference & Autonomous Agentic Ecosystem… See the full description on the dataset page: https://huggingface.co/datasets/Brunobkr/llama.cpp_AlgMor24_github.
03k
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 "nlohmann/json.hpp"15 16using json = nlohmann::ordered_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[]) {28 testing t(std::cout);29 if (argc >= 2) {30 t.set_filter(argv[1]);31 }32 33 const char * verbose = getenv("LLAMA_TEST_VERBOSE");34 if (verbose) {35 t.verbose = std::string(verbose) == "1";36 }37 38 t.test("native", test_example_native);39 t.test("qwen3 coder", test_example_qwen3_coder);40 t.test("qwen3 non-coder", test_example_qwen3_non_coder);41 t.test("comparison", test_command7_parser_compare);42 t.test("prefix tool names", test_prefix_tool_names);43 t.test("tagged peg parser", test_tagged_peg_parser);44 t.test("permute", test_permute);45 46 return t.summary();47}48 49static json create_tools() {50 json tools = json::array();51 52 json 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", { "celsius", "fahrenheit" } },67 { "description",68 "The temperature unit to use. Infer this from the users location." } } } } },69 { "required", { "location", "unit" } },70 } },71 } }72 };73 tools.push_back(tool_weather);74 75 json 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", { "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", { "location", "unit" } },97 } },98 } }99 };100 tools.push_back(tool_forecast);101 102 json 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", { "api", "troubleshooting", "billing", "general" } },118 { "description", "Filter search by specific category." } } } } },119 { "required", { "query", "category" } },120 { "additionalProperties", false } } },121 { "strict", true } } }122 };123 tools.push_back(tool_search);124 125 return tools;126}127 128struct tool_argument {129 std::string name;130 std::string type;131 bool is_required;132 json schema;133};134 135struct tool_definition {136 std::string name;137 std::vector<tool_argument> arguments;138 json schema;139};140 141// Test fictitious model output that emits arguments as JSON.142static void test_example_native(testing & t) {143 struct test_case {144 // Parameters145 std::string name;146 json tools;147 common_chat_tool_choice tool_choice;148 common_reasoning_format reasoning_format;149 json json_schema;150 bool parallel_tool_calls;151 std::string generation_prompt;152 std::string input;153 154 // Expect155 std::string expect_reasoning;156 std::string expect_content;157 std::vector<common_chat_tool_call> expect_tool_calls;158 };159 160 auto build_parser = [](const test_case & tc) {161 return build_chat_peg_parser([&](common_chat_peg_builder & p) {162 auto reasoning_in_content = (tc.reasoning_format == COMMON_REASONING_FORMAT_NONE);163 // Always use optional TAG_BASED pattern; generation_prompt is prepended to input164 auto reasoning = p.optional("<think>" + p.reasoning(p.until("</think>")) + "</think>" + p.space());165 166 // tool calling parser167 if (tc.tools.is_array() && !tc.tools.empty()) {168 auto tool_call =169 p.standard_json_tools("<tool_call>[", "]</tool_call>", tc.tools, tc.parallel_tool_calls,170 tc.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED);171 172 return p.sequence({ (reasoning_in_content ? p.eps() : reasoning), p.content(p.until("<tool_call>")),173 p.optional(p.space() + tool_call), p.space(), p.end() });174 }175 176 // response_format parser177 if (tc.json_schema.is_object() && !tc.json_schema.empty()) {178 return p.sequence({ (reasoning_in_content ? p.eps() : reasoning),179 p.content(p.schema(p.json(), "response-output", tc.json_schema)), p.space(),180 p.end() });181 }182 183 // Content-only parser184 return 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>["278 R"({"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>["301 R"({"name": "get_current_weather", "arguments": {"location": "New York City, NY", "unit": "fahrenheit"}})"302 ", "303 R"({"name": "get_current_weather", "arguments": {"location": "San Francisco, CA", "unit": "fahrenheit"}})"304 ", "305 R"({"name": "get_forecast", "arguments": {"location": "New York City, NY", "unit": "fahrenheit", "days": 3}})"306 ", "307 R"({"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", { "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"349 R"({"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 = */352 R"({"invoice_number": "INV-2025-001", "amount": 1250.50, "due_date": "2025-12-31"})", /* .expect_tool_calls = */ {},353 },354 };355 356 for (const auto & tc : test_cases) {357 t.test(tc.name, [&](testing & t) {358 auto parser = build_parser(tc);359 auto lazy = !tc.tools.empty() && tc.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;360 auto grammar = build_grammar([&](const common_grammar_builder & builder) {361 for (const auto & def : tc.tools) {362 auto function = def.at("function");363 auto parameters = function.at("parameters");364 builder.resolve_refs(parameters);365 };366 parser.build_grammar(builder, lazy);367 });368 369 t.log("Grammar:");370 for (const auto & line : string_split(grammar, "\n")) {371 t.log(line);372 }373 374 std::string effective_input = tc.generation_prompt + tc.input;375 common_peg_parse_context ctx(effective_input);376 auto result = parser.parse(ctx);377 378 t.assert_true("success", result.success());379 380 common_chat_msg msg;381 auto mapper = common_chat_peg_mapper(msg);382 mapper.from_ast(ctx.ast, result);383 384 t.assert_equal("content equal", tc.expect_content, msg.content);385 t.assert_equal("reasoning equal", tc.expect_reasoning, msg.reasoning_content);386 t.assert_equal("number of tool calls", tc.expect_tool_calls.size(), msg.tool_calls.size());387 for (auto i = 0u; i < std::min(tc.expect_tool_calls.size(), msg.tool_calls.size()); i++) {388 t.assert_equal("tool name", tc.expect_tool_calls[i].name, msg.tool_calls[i].name);389 t.assert_equal("tool args", tc.expect_tool_calls[i].arguments, msg.tool_calls[i].arguments);390 }391 });392 }393}394 395static void test_example_qwen3_coder(testing & t) {396 auto tools = create_tools();397 auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {398 auto content = p.rule("content", p.content(p.until("<tool_call>")));399 400 std::vector<common_peg_parser> tool_parsers;401 for (const auto & def : tools) {402 auto function = def.at("function");403 std::string name = function.at("name");404 auto parameters = function.at("parameters");405 auto properties = parameters.at("properties");406 407 std::set<std::string> required_properties;408 if (function.contains("required")) {409 function.at("required").get_to(required_properties);410 }411 412 std::vector<common_peg_parser> arg_parsers;413 for (const auto & [param_name, param_schema] : properties.items()) {414 bool is_required = required_properties.find(param_name) != required_properties.end();415 auto type = param_schema.value("type", "object");416 417 auto arg = p.tool_arg(418 p.sequence({ p.tool_arg_open("<parameter=" + p.tool_arg_name(p.literal(param_name)) + ">"),419 (type == "string" ?420 p.tool_arg_string_value(p.schema(421 p.until_one_of({ "</parameter>\n<parameter=", "</parameter>\n</function>" }),422 "tool-" + name + "-arg-" + param_name + "-schema", param_schema, true)) :423 p.tool_arg_json_value(p.schema(424 p.json(), "tool-" + name + "-arg-" + param_name + "-schema", param_schema))),425 p.tool_arg_close("</parameter>\n" +426 p.peek(p.literal("<parameter=") | p.literal("</function>"))) }));427 428 arg_parsers.push_back(is_required ? p.rule("tool-" + name + "-arg-" + param_name, arg) :429 p.optional(p.rule("tool-" + name + "-arg-" + param_name, arg)));430 }431 432 tool_parsers.push_back(p.rule("tool-" + name, p.tool_open("<function=" + p.tool_name(p.literal(name)) + ">")433 << p.sequence(arg_parsers)434 << p.tool_close(p.literal("</function>"))));435 };436 437 auto tool_call = p.trigger_rule("tool-call", "<tool_call>" << p.choice(tool_parsers) << "</tool_call>");438 439 return content + p.zero_or_more(p.space() + tool_call) + p.end();440 });441 442 auto grammar = build_grammar([&](const common_grammar_builder & builder) {443 for (const auto & def : tools) {444 auto function = def.at("function");445 auto parameters = function.at("parameters");446 builder.resolve_refs(parameters);447 };448 parser.build_grammar(builder);449 });450 451 t.log("Grammar:");452 for (const auto & line : string_split(grammar, "\n")) {453 t.log(line);454 }455 456 t.test("incremental parsing", [&](testing & t) {457 std::string input =458 "Let me search the knowledge base for cat pictures."459 "<tool_call>\n"460 "<function=search_knowledge_base>\n"461 "<parameter=query>cat pictures</parameter>\n"462 "<parameter=category>general</parameter>\n"463 "</function>\n"464 "</tool_call>";465 466 std::vector<std::string> tokens = simple_tokenize(input);467 468 common_chat_msg prev;469 for (auto it = tokens.begin(); it != tokens.end(); it++) {470 std::string in = std::accumulate(tokens.begin(), it + 1, std::string());471 472 common_peg_parse_context ctx(in, (it + 1 < tokens.end()) ? COMMON_PEG_PARSE_FLAG_LENIENT : COMMON_PEG_PARSE_FLAG_NONE);473 474 auto result = parser.parse(ctx);475 if (!t.assert_equal("not fail", false, result.fail())) {476 t.log(in.substr(0, result.end) + "[failed->]" + in.substr(result.end));477 }478 479 common_chat_msg msg;480 auto mapper = common_chat_peg_mapper(msg);481 mapper.from_ast(ctx.ast, result);482 483 //t.log("Input: " + input);484 t.log("===========================================");485 t.log("Iteration " + std::to_string(in.size()));486 t.log("Reasoning: " + msg.reasoning_content);487 t.log("Content : " + msg.content);488 for (const auto & tc : msg.tool_calls) {489 t.log("Tool name: " + tc.name);490 t.log("Tool args: " + tc.arguments);491 }492 493 try {494 // This shouldn't emit any runtime errors495 auto diffs = common_chat_msg_diff::compute_diffs(prev, msg);496 } catch (const std::exception & e) {497 t.log(in.substr(0, result.end) + "[failed->]" + in.substr(result.end));498 t.assert_true(std::string("failed with ") + e.what(), false);499 }500 501 prev = msg;502 }503 });504}505 506static void test_example_qwen3_non_coder(testing & t) {507 auto tools = create_tools();508 auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {509 // tool calling parser using standard JSON format510 auto tool_call = p.standard_json_tools("<tool_call>", "</tool_call>", tools, true, false);511 512 return p.sequence({ p.content(p.until("<tool_call>")), p.optional(p.space() + tool_call), p.end() });513 });514 515 auto grammar = build_grammar([&](const common_grammar_builder & builder) {516 for (const auto & def : tools) {517 auto function = def.at("function");518 auto parameters = function.at("parameters");519 builder.resolve_refs(parameters);520 };521 parser.build_grammar(builder);522 });523 524 t.log("Grammar:");525 for (const auto & line : string_split(grammar, "\n")) {526 t.log(line);527 }528 529 t.test("tool call parsing", [&](testing & t) {530 std::string input =531 "I need to get the weather.\n"532 "<tool_call>"533 "{\"name\": \"get_current_weather\", \"arguments\": {\"location\": \"New York City, NY\", \"unit\": "534 "\"fahrenheit\"}}"535 "</tool_call>";536 537 common_peg_parse_context ctx(input);538 auto result = parser.parse(ctx);539 540 t.assert_true("success", result.success());541 542 common_chat_msg msg;543 auto mapper = common_chat_peg_mapper(msg);544 mapper.from_ast(ctx.ast, result);545 546 t.assert_equal("content", "I need to get the weather.\n", msg.content);547 t.assert_equal("reasoning", "", msg.reasoning_content);548 t.assert_equal("tool calls count", 1u, msg.tool_calls.size());549 if (!msg.tool_calls.empty()) {550 t.assert_equal("tool name", "get_current_weather", msg.tool_calls[0].name);551 t.assert_equal("tool args", "{\"location\": \"New York City, NY\", \"unit\": \"fahrenheit\"}",552 msg.tool_calls[0].arguments);553 }554 });555 556 t.test("incremental parsing", [&](testing & t) {557 std::string input =558 "I need to get the weather.\n"559 "<tool_call>"560 "{\"name\": \"get_current_weather\", \"arguments\": {\"location\": \"New York City, NY\", \"unit\": "561 "\"fahrenheit\"}}"562 "</tool_call>";563 564 std::vector<std::string> tokens = simple_tokenize(input);565 566 common_chat_msg prev;567 for (auto it = tokens.begin(); it != tokens.end(); it++) {568 std::string in = std::accumulate(tokens.begin(), it + 1, std::string());569 570 common_peg_parse_context ctx(in, (it + 1 < tokens.end()) ? COMMON_PEG_PARSE_FLAG_LENIENT : COMMON_PEG_PARSE_FLAG_NONE);571 572 auto result = parser.parse(ctx);573 if (!t.assert_equal("not fail", false, result.fail())) {574 t.log(in.substr(0, result.end) + "[failed->]" + in.substr(result.end));575 }576 577 common_chat_msg msg;578 auto mapper = common_chat_peg_mapper(msg);579 mapper.from_ast(ctx.ast, result);580 581 //t.log("Input: " + input);582 t.log("===========================================");583 t.log("Iteration " + std::to_string(in.size()));584 t.log("Reasoning: " + msg.reasoning_content);585 t.log("Content : " + msg.content);586 for (const auto & tc : msg.tool_calls) {587 t.log("Tool name: " + tc.name);588 t.log("Tool args: " + tc.arguments);589 }590 591 try {592 // This shouldn't emit any runtime errors593 auto diffs = common_chat_msg_diff::compute_diffs(prev, msg);594 } catch (const std::exception & e) {595 t.log(in.substr(0, result.end) + "[failed->]" + in.substr(result.end));596 t.assert_true(std::string("failed with ") + e.what(), false);597 }598 599 prev = msg;600 }601 });602}603 604void test_command7_parser_compare(testing & t) {605 auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {606 auto thinking =607 p.reasoning_block("<|START_THINKING|>" << p.reasoning(p.until("<|END_THINKING|>")) << "<|END_THINKING|>");608 609 auto response = "<|START_RESPONSE|>" << p.content(p.until("<|END_RESPONSE|>")) << "<|END_RESPONSE|>";610 611 auto tool_call_id = p.atomic("\"tool_call_id\"" << (":" << ("\"" + p.tool_id(p.string_content('"')) + "\"")));612 auto tool_call_name =613 p.atomic("\"tool_name\"" << (":" << ("\"" + p.tool_name(p.string_content('"')) + "\"")));614 auto tool_call_args = "\"parameters\"" << (":" << p.tool_args(p.json()));615 616 auto tool_call_fields = p.rule("tool-call-fields", tool_call_id | tool_call_name | tool_call_args);617 auto tool_call =618 p.rule("tool-call", p.tool(p.tool_open(p.literal("{"))619 << tool_call_fields << p.zero_or_more(p.literal(",") << tool_call_fields)620 << p.tool_close(p.literal("}"))));621 622 auto tool_calls = p.rule(623 "tool-calls", "<|START_ACTION|>" << ("[" << tool_call << p.zero_or_more(p.literal(",") << tool_call) << "]")624 << "<|END_ACTION|>");625 626 return p.optional(thinking) << (tool_calls | response) + p.end();627 });628 629 auto test_current = [&](const common_peg_arena & p, const std::string & input, bool is_partial,630 bool print_results) {631 common_peg_parse_context ctx(input, is_partial ? COMMON_PEG_PARSE_FLAG_LENIENT : COMMON_PEG_PARSE_FLAG_NONE);632 auto result = p.parse(ctx);633 634 common_chat_msg msg;635 auto mapper = common_chat_peg_mapper(msg);636 mapper.from_ast(ctx.ast, result);637 638 if (print_results) {639 std::cout << "== Parsed (new) ==\n";640 std::cout << "=== Reasoning ===\n";641 std::cout << msg.reasoning_content << "\n";642 std::cout << "\n\n=== Content ===\n";643 std::cout << msg.content << "\n";644 std::cout << "\n\n=== Tool Calls ===\n";645 for (const auto & tc : msg.tool_calls) {646 std::cout << "id: " << tc.id << "\n";647 std::cout << "name: " << tc.name << "\n";648 std::cout << "args: " << tc.arguments << "\n";649 }650 }651 };652 653 std::string reasoning =654 "To plan an effective trip to Japan that includes both historical sites and modern attractions within a "655 "budget of $4000 for a two-week stay, we need to:\n\n"656 "1. Identify key historical sites and modern attractions in Japan.\n"657 "2. Find affordable accommodation options that provide a balance between comfort and cost.\n"658 "3. Determine the best modes of transportation for getting around Japan.\n"659 "4. Create a day-by-day itinerary that ensures the user gets to see a variety of attractions without "660 "overspending.\n"661 "5. Provide a detailed cost breakdown that includes accommodation, transportation, meals, and entry fees "662 "to attractions.";663 664 std::vector<std::tuple<std::string, std::string, nlohmann::json>> tool_calls = {665 { "call_0", "plan_trip", nlohmann::json::parse(R"({666 "destination": "Japan",667 "duration": 14,668 "budget": 4000,669 "interests": ["historical sites", "modern attractions"],670 "accommodation_preferences": "affordable",671 "transportation_preferences": "efficient",672 "meal_preferences": "local cuisine"673 })") }674 };675 676 std::vector<std::string> tokens;677 678 // Build tokens679 if (!reasoning.empty()) {680 auto tokenized = simple_tokenize(reasoning);681 tokens.emplace_back("<|START_THINKING|>");682 tokens.insert(tokens.end(), tokenized.begin(), tokenized.end());683 tokens.emplace_back("<|END_THINKING|>");684 }685 686 if (!tool_calls.empty()) {687 tokens.emplace_back("<|START_ACTION|>");688 689 auto json = nlohmann::json::array();690 for (const auto & tc : tool_calls) {691 auto tc_json = nlohmann::json::object();692 tc_json["tool_call_id"] = std::get<0>(tc);693 tc_json["tool_name"] = std::get<1>(tc);694 tc_json["parameters"] = std::get<2>(tc);695 json.push_back(tc_json);696 }697 698 auto tokenized = simple_tokenize(json.dump(-1, ' ', true));699 tokens.insert(tokens.end(), tokenized.begin(), tokenized.end());700 701 tokens.emplace_back("<|END_ACTION|>");702 }703 704 std::string input = std::accumulate(tokens.begin(), tokens.end(), std::string());705 706 t.test("current_parse", [&](testing & /* t */) { test_current(parser, input, false, false); });707 t.bench("current_parse_benchmark complete", [&]() { test_current(parser, input, false, false); }, 100);708 t.bench(709 "current_parse_benchmark incremental",710 [&]() {711 std::string in;712 for (auto i = 0u; i < tokens.size(); i++) {713 in += tokens[i];714 test_current(parser, in, i + 1 < tokens.size(), false);715 }716 },717 20);718}719 720// Test that tool names that are proper prefixes of other tool names don't cause721// premature matching during incremental parsing.722// For example, "special_function" should not match when parsing "special_function_with_opt".723static void test_prefix_tool_names(testing & t) {724 // Create tools where one name is a proper prefix of another725 json tools = json::array();726 727 json tool_short = {728 { "type", "function" },729 { "function",730 {731 { "name", "special_function" },732 { "description", "A special function" },733 { "parameters",734 {735 { "type", "object" },736 { "properties",737 {738 { "arg1", { { "type", "integer" } } },739 } },740 { "required", { "arg1" } },741 } },742 } }743 };744 tools.push_back(tool_short);745 746 json tool_long = {747 { "type", "function" },748 { "function",749 {750 { "name", "special_function_with_opt" },751 { "description", "A special function with optional params" },752 { "parameters",753 {754 { "type", "object" },755 { "properties",756 {757 { "arg1", { { "type", "integer" } } },758 { "arg2", { { "type", "integer" } } },759 } },760 { "required", { "arg1" } },761 } },762 } }763 };764 tools.push_back(tool_long);765 766 // Use standard_constructed_tools which had the prefix matching bug767 std::map<std::string, std::string> markers = {768 { "tool_call_start_marker", "<tool_call>" },769 { "tool_call_end_marker", "</tool_call>" },770 { "function_opener", "<function=" },771 { "function_closer", "</function>" },772 { "function_name_suffix", ">" },773 { "parameter_key_prefix", "<param=" },774 { "parameter_key_suffix", ">" },775 { "parameter_closer", "</param>" },776 };777 778 auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {779 auto content = p.rule("content", p.content(p.until("<tool_call>")));780 auto tool_call = p.standard_constructed_tools(markers, tools, false, false);781 return content + p.zero_or_more(p.space() + tool_call) + p.end();782 });783 784 // Test parsing the long tool name - this should NOT trigger the short tool name785 t.test("parse long tool name", [&](testing & t) {786 std::string input =787 "Let me call the function."788 "<tool_call>"789 "<function=special_function_with_opt>"790 "<param=arg1>42</param>"791 "</function>"792 "</tool_call>";793 794 common_peg_parse_context ctx(input);795 auto result = parser.parse(ctx);796 797 t.assert_true("success", result.success());798 799 common_chat_msg msg;800 auto mapper = common_chat_peg_mapper(msg);801 mapper.from_ast(ctx.ast, result);802 803 t.assert_equal("content", "Let me call the function.", msg.content);804 t.assert_equal("tool calls count", 1u, msg.tool_calls.size());805 if (!msg.tool_calls.empty()) {806 t.assert_equal("tool name", "special_function_with_opt", msg.tool_calls[0].name);807 }808 });809 810 // Test incremental parsing - the key test case811 // This ensures that when incrementally parsing "special_function_with_opt",812 // we don't prematurely emit "special_function" as a tool call813 t.test("incremental parse long tool name", [&](testing & t) {814 std::string input =815 "Let me call the function."816 "<tool_call>"817 "<function=special_function_with_opt>"818 "<param=arg1>42</param>"819 "</function>"820 "</tool_call>";821 822 std::vector<std::string> tokens = simple_tokenize(input);823 824 common_chat_msg prev;825 for (auto it = tokens.begin(); it != tokens.end(); it++) {826 std::string in = std::accumulate(tokens.begin(), it + 1, std::string());827 828 common_peg_parse_context ctx(in, (it + 1 < tokens.end()) ? COMMON_PEG_PARSE_FLAG_LENIENT : COMMON_PEG_PARSE_FLAG_NONE);829 auto result = parser.parse(ctx);830 831 if (!t.assert_equal("not fail", false, result.fail())) {832 t.log(in.substr(0, result.end) + "[failed->]" + in.substr(result.end));833 return;834 }835 836 common_chat_msg msg;837 auto mapper = common_chat_peg_mapper(msg);838 mapper.from_ast(ctx.ast, result);839 840 // The critical check: during incremental parsing, we should never841 // see "special_function" as the tool name when parsing "special_function_with_opt"842 for (const auto & tc : msg.tool_calls) {843 if (!t.assert_equal("tool name should not be short prefix", false,844 tc.name == "special_function")) {845 t.log("Premature tool name match at input: " + in);846 return;847 }848 }849 850 try {851 auto diffs = common_chat_msg_diff::compute_diffs(prev, msg);852 } catch (const std::exception & e) {853 t.log(in.substr(0, result.end) + "[failed->]" + in.substr(result.end));854 t.assert_true(std::string("diff failed with ") + e.what(), false);855 return;856 }857 858 prev = msg;859 }860 861 // Final check: the complete parse should have the correct tool name862 t.assert_equal("final tool calls count", 1u, prev.tool_calls.size());863 if (!prev.tool_calls.empty()) {864 t.assert_equal("final tool name", "special_function_with_opt", prev.tool_calls[0].name);865 }866 });867 868 // Test parsing the short tool name still works869 t.test("parse short tool name", [&](testing & t) {870 std::string input =871 "Let me call the function."872 "<tool_call>"873 "<function=special_function>"874 "<param=arg1>42</param>"875 "</function>"876 "</tool_call>";877 878 common_peg_parse_context ctx(input);879 auto result = parser.parse(ctx);880 881 t.assert_true("success", result.success());882 883 common_chat_msg msg;884 auto mapper = common_chat_peg_mapper(msg);885 mapper.from_ast(ctx.ast, result);886 887 t.assert_equal("content", "Let me call the function.", msg.content);888 t.assert_equal("tool calls count", 1u, msg.tool_calls.size());889 if (!msg.tool_calls.empty()) {890 t.assert_equal("tool name", "special_function", msg.tool_calls[0].name);891 }892 });893}894 895static void test_tagged_peg_parser(testing & t) {896 t.test("basic tag extraction", [&](testing & t) {897 auto parser = build_tagged_peg_parser([](common_peg_parser_builder & p) {898 return p.tag("greeting", p.until(" ")) + " " + p.tag("name", p.rest()) + p.end();899 });900 901 auto result = parser.parse_and_extract("Hello World");902 t.assert_true("success", result.result.success());903 t.assert_equal("greeting tag", "Hello", result.tags.at("greeting"));904 t.assert_equal("name tag", "World", result.tags.at("name"));905 });906 907 t.test("duplicate tags overwrite", [&](testing & t) {908 auto parser = build_tagged_peg_parser([](common_peg_parser_builder & p) {909 return p.tag("item", p.until(",")) + "," + p.tag("item", p.rest()) + p.end();910 });911 912 auto result = parser.parse_and_extract("first,second");913 t.assert_true("success", result.result.success());914 t.assert_equal("item tag", "second", result.tags.at("item"));915 });916 917 t.test("no tags extracted", [&](testing & t) {918 auto parser = build_tagged_peg_parser([](common_peg_parser_builder & p) {919 return p.rest() + p.end();920 });921 922 auto result = parser.parse_and_extract("Hello");923 t.assert_true("success", result.result.success());924 t.assert_equal("empty tags", 0u, result.tags.size());925 });926 927 t.test("structured extraction", [&](testing & t) {928 auto parser = build_tagged_peg_parser([](common_peg_parser_builder & p) {929 auto header = p.tag("header", p.until("\n"));930 auto body = p.tag("body", p.rest());931 return header + "\n" + body + p.end();932 });933 934 auto result = parser.parse_and_extract("Title\nBody content here");935 t.assert_true("success", result.result.success());936 t.assert_equal("header", "Title", result.tags.at("header"));937 t.assert_equal("body", "Body content here", result.tags.at("body"));938 });939 940 t.test("partial parse", [&](testing & t) {941 auto parser = build_tagged_peg_parser([](common_peg_parser_builder & p) {942 return p.tag("prefix", p.until(":")) + ":" + p.tag("value", p.rest()) + p.end();943 });944 945 auto result = parser.parse_and_extract("key:val", COMMON_PEG_PARSE_FLAG_LENIENT);946 t.assert_true("not fail", !result.result.fail());947 t.assert_equal("prefix tag", "key", result.tags.at("prefix"));948 t.assert_equal("value tag", "val", result.tags.at("value"));949 });950 951 t.test("find in the middle", [&](testing & t) {952 auto parser = build_tagged_peg_parser([](common_peg_parser_builder & p) {953 return p.choice({ p.literal("{"), p.literal(":") }) + p.space() + p.literal("\"") + p.atomic(p.literal("fun_name"));954 });955 956 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>";957 auto result = parser.parse_anywhere_and_extract(tpl);958 t.assert_true("success", result.result.success());959 });960 961 t.test("fail find in the middle", [&](testing & t) {962 auto parser = build_tagged_peg_parser([](common_peg_parser_builder & p) {963 return p.choice({ p.literal("{"), p.literal(":") }) + p.space() + p.literal("\"") + p.atomic(p.literal("fun_name"));964 });965 966 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>";967 auto result = parser.parse_anywhere_and_extract(tpl);968 t.assert_true("failure", result.result.fail());969 });970 971 t.test("find function tag with name", [&](testing &t) {972 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";973 auto parser = build_tagged_peg_parser([](common_peg_parser_builder & p) {974 std::string needle = "foofoo";975 return p.tag("fun_marker", p.choice({976 p.tag("fun_pre", p.literal("<") + p.until_one_of({ ">", needle })) + p.literal(needle) +977 p.tag("fun_post", p.negate(p.space() + p.literal("<")) + p.until(">") + p.literal(">")) + p.space(),978 p.tag("fun_pre", p.literal("[") + p.until_one_of({ "]", needle })) + p.literal(needle) +979 p.tag("fun_post", p.negate(p.space() + p.literal("[") + p.until("]") + p.literal("]")) + p.space()) }));980 });981 auto result = parser.parse_anywhere_and_extract(haystack);982 t.assert_true("success", result.result.success());983 t.assert_equal("fun_pre should be '<function='", "<function=", result.tags["fun_pre"]);984 t.assert_equal("fun_post should be '>'", ">", result.tags["fun_post"]);985 });986}987 988static void test_permute(testing & t) {989 auto accepts = [](const common_peg_arena & parser, const std::string & input) {990 common_peg_parse_context ctx(input);991 return parser.parse(ctx).success();992 };993 994 auto gbnf_of = [](const common_peg_arena & parser) {995 return build_grammar([&](const common_grammar_builder & builder) { parser.build_grammar(builder); });996 };997 998 auto assert_gbnf_equal = [](testing & t, const std::string & expected, const std::string & actual) {999 static const std::regex leading_ws_re = std::regex(R"((^|\n)\s+)");1000 t.assert_equal("gbnf are equal", std::regex_replace(expected, leading_ws_re, "$1"), actual);1001 };1002 1003 auto count_rules = [](const std::string & gbnf, const std::string & prefix) {1004 size_t count = 0;1005 for (const auto & line : string_split<std::string>(gbnf, '\n')) {1006 if (line.rfind(prefix, 0) == 0) {1007 count++;1008 }1009 }1010 return count;1011 };1012 1013 t.test("accepts every ordering", [&](testing & t) {1014 auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {1015 return p.permute("abc", { p.literal("a"), p.literal("b"), p.literal("c") }) + p.end();1016 });1017 1018 for (const std::string input : { "abc", "acb", "bac", "bca", "cab", "cba" }) {1019 t.assert_true("accepts " + input, accepts(parser, input));1020 }1021 });1022 1023 t.test("single element", [&](testing & t) {1024 auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {1025 return p.permute("a", { p.literal("a") }) + p.end();1026 });1027 1028 t.assert_true("accepts a", accepts(parser, "a"));1029 t.assert_true("rejects aa", !accepts(parser, "aa"));1030 });1031 1032 t.test("grammar left-factorizes shared tails", [&](testing & t) {1033 auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {1034 return p.permute("abc", { p.literal("a"), p.literal("b"), p.literal("c") }) + p.end();1035 });1036 1037 // Every rule is one remaining subset, keyed by bitmask: abc-3 is {a,b}, abc-7 is {a,b,c}.1038 // Each subset is emitted once and shared by every branch that leads into it.1039 assert_gbnf_equal(t, R"""(1040 abc-1 ::= "a"1041 abc-2 ::= "b"1042 abc-3 ::= "a" abc-2 | "b" abc-11043 abc-4 ::= "c"1044 abc-5 ::= "a" abc-4 | "c" abc-11045 abc-6 ::= "b" abc-4 | "c" abc-21046 abc-7 ::= "a" abc-6 | "b" abc-5 | "c" abc-31047 root ::= abc-71048 space ::= | " " | "\n"{1,2} [ \t]{0,20}1049 )""", gbnf_of(parser));1050 });1051 1052 t.test("grammar emits one rule per remaining subset", [&](testing & t) {1053 auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {1054 return p.permute("abcd", { p.literal("a"), p.literal("b"), p.literal("c"), p.literal("d") }) + p.end();1055 });1056 1057 // 2^4 - 1 non-empty subsets, one rule each - not the 4! = 24 orderings.1058 t.assert_equal("permute rule count", 15u, count_rules(gbnf_of(parser), "abcd-"));1059 });1060 1061 t.test("grammar emits no rules for a single element", [&](testing & t) {1062 auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {1063 return p.permute("a", { p.literal("a") }) + p.end();1064 });1065 1066 assert_gbnf_equal(t, R"""(1067 root ::= "a"1068 space ::= | " " | "\n"{1,2} [ \t]{0,20}1069 )""", gbnf_of(parser));1070 });1071 1072 t.test("grammar falls back to the given order when too large", [&](testing & t) {1073 auto parser = build_chat_peg_parser([](common_chat_peg_builder & p) {1074 std::vector<common_peg_parser> parsers;1075 for (size_t i = 0; i <= COMMON_CHAT_MAX_PERMUTE; i++) {1076 parsers.push_back(p.literal(std::string(1, (char) ('a' + i))));1077 }1078 return p.permute("big", parsers) + p.end();1079 });1080 1081 assert_gbnf_equal(t, R"""(1082 root ::= "a" "b" "c" "d" "e" "f" "g"1083 space ::= | " " | "\n"{1,2} [ \t]{0,20}1084 )""", gbnf_of(parser));1085 });1086}1087 