mirror of
				https://github.com/ggml-org/llama.cpp.git
				synced 2025-10-30 08:42:00 +00:00 
			
		
		
		
	chat: Add LFM2 tool handling (#16763)
* Add LFM2 tool handling * fmt * Apply suggestion from @ykhrustalev
This commit is contained in:
		
							
								
								
									
										198
									
								
								common/chat.cpp
									
									
									
									
									
								
							
							
						
						
									
										198
									
								
								common/chat.cpp
									
									
									
									
									
								
							| @@ -9,8 +9,11 @@ | |||||||
| #include <minja/chat-template.hpp> | #include <minja/chat-template.hpp> | ||||||
| #include <minja/minja.hpp> | #include <minja/minja.hpp> | ||||||
|  |  | ||||||
|  | #include <algorithm> | ||||||
| #include <cstdio> | #include <cstdio> | ||||||
|  | #include <cctype> | ||||||
| #include <exception> | #include <exception> | ||||||
|  | #include <functional> | ||||||
| #include <iostream> | #include <iostream> | ||||||
| #include <optional> | #include <optional> | ||||||
| #include <stdexcept> | #include <stdexcept> | ||||||
| @@ -640,6 +643,7 @@ const char * common_chat_format_name(common_chat_format format) { | |||||||
|         case COMMON_CHAT_FORMAT_SEED_OSS: return "Seed-OSS"; |         case COMMON_CHAT_FORMAT_SEED_OSS: return "Seed-OSS"; | ||||||
|         case COMMON_CHAT_FORMAT_NEMOTRON_V2: return "Nemotron V2"; |         case COMMON_CHAT_FORMAT_NEMOTRON_V2: return "Nemotron V2"; | ||||||
|         case COMMON_CHAT_FORMAT_APERTUS: return "Apertus"; |         case COMMON_CHAT_FORMAT_APERTUS: return "Apertus"; | ||||||
|  |         case COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS: return "LFM2 with JSON tools"; | ||||||
|         default: |         default: | ||||||
|             throw std::runtime_error("Unknown chat format"); |             throw std::runtime_error("Unknown chat format"); | ||||||
|     } |     } | ||||||
| @@ -986,6 +990,126 @@ static common_chat_params common_chat_params_init_mistral_nemo(const common_chat | |||||||
|     return data; |     return data; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // Case-insensitive find | ||||||
|  | static size_t ifind_string(const std::string & haystack, const std::string & needle, size_t pos = 0) { | ||||||
|  |     auto it = std::search( | ||||||
|  |         haystack.begin() + pos, haystack.end(), | ||||||
|  |         needle.begin(), needle.end(), | ||||||
|  |         [](char a, char b) { return std::tolower(a) == std::tolower(b); } | ||||||
|  |     ); | ||||||
|  |     return (it == haystack.end()) ? std::string::npos : std::distance(haystack.begin(), it); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static common_chat_params common_chat_params_init_lfm2(const common_chat_template & tmpl, const struct templates_params & inputs) { | ||||||
|  |     common_chat_params data; | ||||||
|  |     const auto is_json_schema_provided = !inputs.json_schema.is_null(); | ||||||
|  |     const auto is_grammar_provided = !inputs.grammar.empty(); | ||||||
|  |     const auto are_tools_provided = inputs.tools.is_array() && !inputs.tools.empty(); | ||||||
|  |  | ||||||
|  |     // the logic requires potentially modifying the messages | ||||||
|  |     auto tweaked_messages = inputs.messages; | ||||||
|  |  | ||||||
|  |     auto replace_json_schema_marker = [](json & messages) -> bool { | ||||||
|  |         static std::string marker1 = "force json schema.\n"; | ||||||
|  |         static std::string marker2 = "force json schema."; | ||||||
|  |  | ||||||
|  |         if (messages.empty() || messages.at(0).at("role") != "system") { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         std::string content = messages.at(0).at("content"); | ||||||
|  |  | ||||||
|  |         for (const auto & marker : {marker1, marker2}) { | ||||||
|  |             const auto pos = ifind_string(content, marker); | ||||||
|  |             if (pos != std::string::npos) { | ||||||
|  |                 content.replace(pos, marker.length(), ""); | ||||||
|  |                 // inject modified content back into the messages | ||||||
|  |                 messages.at(0).at("content") = content; | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return false; | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Lfm2 model does not natively work with json, but can generally understand the tools structure | ||||||
|  |     // | ||||||
|  |     // Example of the pytorch dialog structure: | ||||||
|  |     //     <|startoftext|><|im_start|>system | ||||||
|  |     //     List of tools: <|tool_list_start|>[{"name": "get_candidate_status", "description": "Retrieves the current status of a candidate in the recruitment process", "parameters": {"type": "object", "properties": {"candidate_id": {"type": "string", "description": "Unique identifier for the candidate"}}, "required": ["candidate_id"]}}]<|tool_list_end|><|im_end|> | ||||||
|  |     //     <|im_start|>user | ||||||
|  |     //     What is the current status of candidate ID 12345?<|im_end|> | ||||||
|  |     //     <|im_start|>assistant | ||||||
|  |     //     <|tool_call_start|>[get_candidate_status(candidate_id="12345")]<|tool_call_end|>Checking the current status of candidate ID 12345.<|im_end|> | ||||||
|  |     //     <|im_start|>tool | ||||||
|  |     //     <|tool_response_start|>{"candidate_id": "12345", "status": "Interview Scheduled", "position": "Clinical Research Associate", "date": "2023-11-20"}<|tool_response_end|><|im_end|> | ||||||
|  |     //     <|im_start|>assistant | ||||||
|  |     //     The candidate with ID 12345 is currently in the "Interview Scheduled" stage for the position of Clinical Research Associate, with an interview date set for 2023-11-20.<|im_end|> | ||||||
|  |     // | ||||||
|  |     // For the llama server compatibility with json tools semantic, | ||||||
|  |     // the client can add "Follow json schema." line into the system message prompt to force the json output. | ||||||
|  |     // | ||||||
|  |     if (are_tools_provided && (is_json_schema_provided || is_grammar_provided)) { | ||||||
|  |         // server/utils.hpp prohibits that branch for the custom grammar anyways | ||||||
|  |         throw std::runtime_error("Tools call must not use \"json_schema\" or \"grammar\", use non-tool invocation if you want to use custom grammar"); | ||||||
|  |     } else if (are_tools_provided && replace_json_schema_marker(tweaked_messages)) { | ||||||
|  |         LOG_INF("%s: Using tools to build a grammar\n", __func__); | ||||||
|  |  | ||||||
|  |         data.grammar = build_grammar([&](const common_grammar_builder & builder) { | ||||||
|  |             auto schemas = json::array(); | ||||||
|  |             foreach_function(inputs.tools, [&](const json & tool) { | ||||||
|  |                 const auto & function = tool.at("function"); | ||||||
|  |                 schemas.push_back({ | ||||||
|  |                     {"type", "object"}, | ||||||
|  |                     {"properties", { | ||||||
|  |                         {"name", { | ||||||
|  |                             {"type", "string"}, | ||||||
|  |                             {"const", function.at("name")}, | ||||||
|  |                         }}, | ||||||
|  |                         {"arguments", function.at("parameters")}, | ||||||
|  |                     }}, | ||||||
|  |                     {"required", json::array({"name", "arguments", "id"})}, | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |             auto schema = json { | ||||||
|  |                 {"type", "array"}, | ||||||
|  |                 {"items", schemas.size() == 1 ? schemas[0] : json {{"anyOf", schemas}}}, | ||||||
|  |                 {"minItems", 1}, | ||||||
|  |             }; | ||||||
|  |             if (!inputs.parallel_tool_calls) { | ||||||
|  |                 schema["maxItems"] = 1; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             builder.add_rule("root", "\"<|tool_call_start|>\"" + builder.add_schema("tool_calls", schema) + "\"<|tool_call_end|>\""); | ||||||
|  |         }); | ||||||
|  |         // model has no concept of tool selection mode choice, | ||||||
|  |         // if the system prompt rendered correctly it will produce a tool call | ||||||
|  |         // the grammar goes inside the tool call body | ||||||
|  |         data.grammar_lazy = true; | ||||||
|  |         data.grammar_triggers = {{COMMON_GRAMMAR_TRIGGER_TYPE_PATTERN_FULL, "\\s*<\\|tool_call_start\\|>\\s*\\["}}; | ||||||
|  |         data.preserved_tokens = {"<|tool_call_start|>", "<|tool_call_end|>"}; | ||||||
|  |         data.format = COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS; | ||||||
|  |     } else if (are_tools_provided && (!is_json_schema_provided && !is_grammar_provided)) { | ||||||
|  |         LOG_INF("%s: Using tools without json schema or grammar\n", __func__); | ||||||
|  |         // output those tokens | ||||||
|  |         data.preserved_tokens = {"<|tool_call_start|>", "<|tool_call_end|>"}; | ||||||
|  |     } else if (is_json_schema_provided) { | ||||||
|  |         LOG_INF("%s: Using provided json schema to build a grammar\n", __func__); | ||||||
|  |         data.grammar = json_schema_to_grammar(inputs.json_schema); | ||||||
|  |     } else if (is_grammar_provided) { | ||||||
|  |         LOG_INF("%s: Using provided grammar\n", __func__); | ||||||
|  |         data.grammar = inputs.grammar; | ||||||
|  |     } else { | ||||||
|  |         LOG_INF("%s: Using content relying on the template\n", __func__); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     data.prompt = apply(tmpl, inputs, /* messages_override= */ tweaked_messages); | ||||||
|  |     LOG_DBG("%s: Prompt: %s\n", __func__, data.prompt.c_str()); | ||||||
|  |  | ||||||
|  |     return data; | ||||||
|  | } | ||||||
|  |  | ||||||
| static common_chat_params common_chat_params_init_magistral(const common_chat_template & tmpl, const struct templates_params & inputs) { | static common_chat_params common_chat_params_init_magistral(const common_chat_template & tmpl, const struct templates_params & inputs) { | ||||||
|     common_chat_params data; |     common_chat_params data; | ||||||
|     data.prompt = apply(tmpl, inputs); |     data.prompt = apply(tmpl, inputs); | ||||||
| @@ -2499,6 +2623,71 @@ static void common_chat_parse_apertus(common_chat_msg_parser & builder) { | |||||||
|     builder.add_content(builder.consume_rest()); |     builder.add_content(builder.consume_rest()); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | static void common_chat_parse_lfm2(common_chat_msg_parser & builder) { | ||||||
|  |     if (!builder.syntax().parse_tool_calls) { | ||||||
|  |         builder.add_content(builder.consume_rest()); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // LFM2 format: <|tool_call_start|>[{"name": "get_current_time", "arguments": {"location": "Paris"}}]<|tool_call_end|> | ||||||
|  |     static const common_regex tool_call_start_regex(regex_escape("<|tool_call_start|>")); | ||||||
|  |     static const common_regex tool_call_end_regex(regex_escape("<|tool_call_end|>")); | ||||||
|  |  | ||||||
|  |     // Loop through all tool calls | ||||||
|  |     while (auto res = builder.try_find_regex(tool_call_start_regex, std::string::npos, /* add_prelude_to_content= */ true)) { | ||||||
|  |         builder.move_to(res->groups[0].end); | ||||||
|  |  | ||||||
|  |         // Parse JSON array format: [{"name": "...", "arguments": {...}}] | ||||||
|  |         auto tool_calls_data = builder.consume_json(); | ||||||
|  |  | ||||||
|  |         // Consume end marker | ||||||
|  |         builder.consume_spaces(); | ||||||
|  |         if (!builder.try_consume_regex(tool_call_end_regex)) { | ||||||
|  |             throw common_chat_msg_partial_exception("Expected <|tool_call_end|>"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Process each tool call in the array | ||||||
|  |         if (tool_calls_data.json.is_array()) { | ||||||
|  |             for (const auto & tool_call : tool_calls_data.json) { | ||||||
|  |                 if (!tool_call.is_object()) { | ||||||
|  |                     throw common_chat_msg_partial_exception("Tool call must be an object"); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if (!tool_call.contains("name")) { | ||||||
|  |                     throw common_chat_msg_partial_exception("Tool call missing 'name' field"); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 std::string function_name = tool_call.at("name"); | ||||||
|  |                 std::string arguments = "{}"; | ||||||
|  |  | ||||||
|  |                 if (tool_call.contains("arguments")) { | ||||||
|  |                     if (tool_call.at("arguments").is_object()) { | ||||||
|  |                         arguments = tool_call.at("arguments").dump(); | ||||||
|  |                     } else if (tool_call.at("arguments").is_string()) { | ||||||
|  |                         arguments = tool_call.at("arguments"); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if (!builder.add_tool_call(function_name, "", arguments)) { | ||||||
|  |                     throw common_chat_msg_partial_exception("Incomplete tool call"); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             throw common_chat_msg_partial_exception("Expected JSON array for tool calls"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Consume any trailing whitespace after this tool call | ||||||
|  |         builder.consume_spaces(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Consume any remaining content after all tool calls | ||||||
|  |     auto remaining = builder.consume_rest(); | ||||||
|  |     if (!string_strip(remaining).empty()) { | ||||||
|  |         builder.add_content(remaining); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| static void common_chat_parse_seed_oss(common_chat_msg_parser & builder) { | static void common_chat_parse_seed_oss(common_chat_msg_parser & builder) { | ||||||
|     // Parse thinking tags first - this handles the main reasoning content |     // Parse thinking tags first - this handles the main reasoning content | ||||||
|     builder.try_parse_reasoning("<seed:think>", "</seed:think>"); |     builder.try_parse_reasoning("<seed:think>", "</seed:think>"); | ||||||
| @@ -2748,6 +2937,12 @@ static common_chat_params common_chat_templates_apply_jinja( | |||||||
|         return common_chat_params_init_apertus(tmpl, params); |         return common_chat_params_init_apertus(tmpl, params); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // LFM2 (w/ tools) | ||||||
|  |     if (src.find("List of tools: <|tool_list_start|>[") != std::string::npos && | ||||||
|  |         src.find("]<|tool_list_end|>") != std::string::npos) { | ||||||
|  |         return common_chat_params_init_lfm2(tmpl, params); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Use generic handler when mixing tools + JSON schema. |     // Use generic handler when mixing tools + JSON schema. | ||||||
|     // TODO: support that mix in handlers below. |     // TODO: support that mix in handlers below. | ||||||
|     if ((params.tools.is_array() && params.json_schema.is_object())) { |     if ((params.tools.is_array() && params.json_schema.is_object())) { | ||||||
| @@ -2926,6 +3121,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) { | |||||||
|         case COMMON_CHAT_FORMAT_APERTUS: |         case COMMON_CHAT_FORMAT_APERTUS: | ||||||
|             common_chat_parse_apertus(builder); |             common_chat_parse_apertus(builder); | ||||||
|             break; |             break; | ||||||
|  |         case COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS: | ||||||
|  |             common_chat_parse_lfm2(builder); | ||||||
|  |             break; | ||||||
|         default: |         default: | ||||||
|             throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format)); |             throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format)); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -116,6 +116,7 @@ enum common_chat_format { | |||||||
|     COMMON_CHAT_FORMAT_SEED_OSS, |     COMMON_CHAT_FORMAT_SEED_OSS, | ||||||
|     COMMON_CHAT_FORMAT_NEMOTRON_V2, |     COMMON_CHAT_FORMAT_NEMOTRON_V2, | ||||||
|     COMMON_CHAT_FORMAT_APERTUS, |     COMMON_CHAT_FORMAT_APERTUS, | ||||||
|  |     COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS, | ||||||
|  |  | ||||||
|     COMMON_CHAT_FORMAT_COUNT, // Not a format, just the # formats |     COMMON_CHAT_FORMAT_COUNT, // Not a format, just the # formats | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								models/templates/llama-cpp-lfm2.jinja
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								models/templates/llama-cpp-lfm2.jinja
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | {{- bos_token -}} | ||||||
|  | {%- set system_prompt = "" -%} | ||||||
|  | {%- set ns = namespace(system_prompt="") -%} | ||||||
|  | {%- if messages[0]["role"] == "system" -%} | ||||||
|  | 	{%- set ns.system_prompt = messages[0]["content"] -%} | ||||||
|  | 	{%- set messages = messages[1:] -%} | ||||||
|  | {%- endif -%} | ||||||
|  | {%- if tools -%} | ||||||
|  | 	{%- set ns.system_prompt = ns.system_prompt + ("\n" if ns.system_prompt else "") + "List of tools: <|tool_list_start|>[" -%} | ||||||
|  | 	{%- for tool in tools -%} | ||||||
|  | 		{%- if tool is not string -%} | ||||||
|  | 			{%- set tool = tool | tojson -%} | ||||||
|  | 		{%- endif -%} | ||||||
|  | 		{%- set ns.system_prompt = ns.system_prompt + tool -%} | ||||||
|  | 		{%- if not loop.last -%} | ||||||
|  | 			{%- set ns.system_prompt = ns.system_prompt + ", " -%} | ||||||
|  | 		{%- endif -%} | ||||||
|  | 	{%- endfor -%} | ||||||
|  | 	{%- set ns.system_prompt = ns.system_prompt + "]<|tool_list_end|>" -%} | ||||||
|  | {%- endif -%} | ||||||
|  | {%- if ns.system_prompt -%} | ||||||
|  | 	{{- "<|im_start|>system\n" + ns.system_prompt + "<|im_end|>\n" -}} | ||||||
|  | {%- endif -%} | ||||||
|  | {%- for message in messages -%} | ||||||
|  | 	{{- "<|im_start|>" + message["role"] + "\n" -}} | ||||||
|  | 	{%- set content = message["content"] -%} | ||||||
|  | 	{%- if content is not string -%} | ||||||
|  | 		{%- set content = content | tojson -%} | ||||||
|  | 	{%- endif -%} | ||||||
|  | 	{%- if message["role"] == "tool" -%} | ||||||
|  | 		{%- set content = "<|tool_response_start|>" + content + "<|tool_response_end|>" -%} | ||||||
|  | 	{%- endif -%} | ||||||
|  | 	{{- content + "<|im_end|>\n" -}} | ||||||
|  | {%- endfor -%} | ||||||
|  | {%- if add_generation_prompt -%} | ||||||
|  | 	{{- "<|im_start|>assistant\n" -}} | ||||||
|  | {%- endif -%} | ||||||
| @@ -16,6 +16,7 @@ | |||||||
|  |  | ||||||
| #include <fstream> | #include <fstream> | ||||||
| #include <iostream> | #include <iostream> | ||||||
|  | #include <functional> | ||||||
| #include <string> | #include <string> | ||||||
|  |  | ||||||
| using json = nlohmann::ordered_json; | using json = nlohmann::ordered_json; | ||||||
| @@ -2138,6 +2139,154 @@ static void test_template_output_parsers() { | |||||||
|  |  | ||||||
|         assert_equals(true, common_chat_templates_support_enable_thinking(tmpls.get())); |         assert_equals(true, common_chat_templates_support_enable_thinking(tmpls.get())); | ||||||
|     } |     } | ||||||
|  |     { | ||||||
|  |         // LFM2 format tests | ||||||
|  |         auto tmpls = read_templates("models/templates/llama-cpp-lfm2.jinja"); | ||||||
|  |         std::vector<std::string> end_tokens{ "<|im_end|>" }; | ||||||
|  |  | ||||||
|  |         auto inputs_tools_forced_json_schema = std::invoke([&]() -> common_chat_templates_inputs { | ||||||
|  |             common_chat_templates_inputs inputs; | ||||||
|  |             inputs.messages = { | ||||||
|  |                 std::invoke([&]() -> common_chat_msg { | ||||||
|  |                     common_chat_msg msg; | ||||||
|  |                     msg.role = "system"; | ||||||
|  |                     msg.content = "force json schema.\n"; | ||||||
|  |                     return msg; | ||||||
|  |                 }), | ||||||
|  |                 message_user, | ||||||
|  |             }; | ||||||
|  |             inputs.tools = {special_function_tool}; | ||||||
|  |             return inputs; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         { | ||||||
|  |             auto params = common_chat_templates_apply(tmpls.get(), inputs_no_tools); | ||||||
|  |             assert_equals(COMMON_CHAT_FORMAT_CONTENT_ONLY, params.format); | ||||||
|  |             assert_equals(false, params.grammar_lazy); | ||||||
|  |             assert_equals(std::string(R"(<|im_start|>user | ||||||
|  | Hey there!<|im_end|> | ||||||
|  | <|im_start|>assistant | ||||||
|  | )"), params.prompt); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         { | ||||||
|  |             auto params = common_chat_templates_apply(tmpls.get(), inputs_tools); | ||||||
|  |             assert_equals(COMMON_CHAT_FORMAT_CONTENT_ONLY, params.format); | ||||||
|  |             assert_equals(false, params.grammar_lazy); | ||||||
|  |             assert_equals(std::string(R"(<|im_start|>system | ||||||
|  | List of tools: <|tool_list_start|>[{"type": "function", "function": {"name": "special_function", "description": "I'm special", "parameters": {"type": "object", "properties": {"arg1": {"type": "integer", "description": "The arg."}}, "required": ["arg1"]}}}]<|tool_list_end|><|im_end|> | ||||||
|  | <|im_start|>user | ||||||
|  | Hey there!<|im_end|> | ||||||
|  | <|im_start|>assistant | ||||||
|  | )"), params.prompt); | ||||||
|  |             assert_equals(true, params.grammar.empty()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         { | ||||||
|  |             auto params = common_chat_templates_apply(tmpls.get(), inputs_tools_forced_json_schema); | ||||||
|  |             assert_equals(COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS, params.format); | ||||||
|  |             assert_equals(true, params.grammar_lazy); | ||||||
|  |             assert_equals(std::string(R"(<|im_start|>system | ||||||
|  | List of tools: <|tool_list_start|>[{"type": "function", "function": {"name": "special_function", "description": "I'm special", "parameters": {"type": "object", "properties": {"arg1": {"type": "integer", "description": "The arg."}}, "required": ["arg1"]}}}]<|tool_list_end|><|im_end|> | ||||||
|  | <|im_start|>user | ||||||
|  | Hey there!<|im_end|> | ||||||
|  | <|im_start|>assistant | ||||||
|  | )"), params.prompt); | ||||||
|  |             assert_equals(false, params.grammar.empty()); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Test parsing regular content | ||||||
|  |         assert_msg_equals(message_assist, | ||||||
|  |             common_chat_parse( | ||||||
|  |                 "Hello, world!\nWhat's up?", | ||||||
|  |                 /* is_partial= */ false, | ||||||
|  |                 {COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS})); | ||||||
|  |  | ||||||
|  |         // Test single tool call with JSON format | ||||||
|  |         common_chat_msg msg_single_tool_call; | ||||||
|  |         msg_single_tool_call.role = "assistant"; | ||||||
|  |         msg_single_tool_call.tool_calls.push_back({"special_function", "{\"arg1\":1}", ""}); | ||||||
|  |         assert_msg_equals( | ||||||
|  |             msg_single_tool_call, | ||||||
|  |             common_chat_parse( | ||||||
|  |                 "<|tool_call_start|>[{\"name\": \"special_function\", \"arguments\": {\"arg1\": 1}}]<|tool_call_end|>", | ||||||
|  |                 /* is_partial= */ false, | ||||||
|  |                 {COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS})); | ||||||
|  |  | ||||||
|  |         // Test tool call with string argument | ||||||
|  |         common_chat_msg msg_tool_call_string; | ||||||
|  |         msg_tool_call_string.role = "assistant"; | ||||||
|  |         msg_tool_call_string.tool_calls.push_back({"get_weather", "{\"location\":\"Paris\"}", ""}); | ||||||
|  |         assert_msg_equals( | ||||||
|  |             msg_tool_call_string, | ||||||
|  |             common_chat_parse( | ||||||
|  |                 "<|tool_call_start|>[{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}]<|tool_call_end|>", | ||||||
|  |                 /* is_partial= */ false, | ||||||
|  |                 {COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS})); | ||||||
|  |  | ||||||
|  |         // Test tool call with multiple arguments | ||||||
|  |         common_chat_msg msg_multi_args; | ||||||
|  |         msg_multi_args.role = "assistant"; | ||||||
|  |         msg_multi_args.tool_calls.push_back({"calculate", "{\"x\":10,\"y\":20,\"operation\":\"add\"}", ""}); | ||||||
|  |         assert_msg_equals( | ||||||
|  |             msg_multi_args, | ||||||
|  |             common_chat_parse( | ||||||
|  |                 "<|tool_call_start|>[{\"name\": \"calculate\", \"arguments\": {\"x\": 10, \"y\": 20, \"operation\": \"add\"}}]<|tool_call_end|>", | ||||||
|  |                 /* is_partial= */ false, | ||||||
|  |                 {COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS})); | ||||||
|  |  | ||||||
|  |         // Test multiple tool calls in single array | ||||||
|  |         common_chat_msg msg_multiple_tools; | ||||||
|  |         msg_multiple_tools.role = "assistant"; | ||||||
|  |         msg_multiple_tools.tool_calls.push_back({"get_weather", "{\"location\":\"Paris\"}", ""}); | ||||||
|  |         msg_multiple_tools.tool_calls.push_back({"get_time", "{\"timezone\":\"UTC\"}", ""}); | ||||||
|  |         assert_msg_equals( | ||||||
|  |             msg_multiple_tools, | ||||||
|  |             common_chat_parse( | ||||||
|  |                 "<|tool_call_start|>[{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}, {\"name\": \"get_time\", \"arguments\": {\"timezone\": \"UTC\"}}]<|tool_call_end|>", | ||||||
|  |                 /* is_partial= */ false, | ||||||
|  |                 {COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS})); | ||||||
|  |  | ||||||
|  |         // Test tool call with content before | ||||||
|  |         common_chat_msg msg_content_before_tool; | ||||||
|  |         msg_content_before_tool.role = "assistant"; | ||||||
|  |         msg_content_before_tool.content = "Let me check the weather for you."; | ||||||
|  |         msg_content_before_tool.tool_calls.push_back({"get_weather", "{\"location\":\"Paris\"}", ""}); | ||||||
|  |         assert_msg_equals( | ||||||
|  |             msg_content_before_tool, | ||||||
|  |             common_chat_parse( | ||||||
|  |                 "Let me check the weather for you.<|tool_call_start|>[{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}]<|tool_call_end|>", | ||||||
|  |                 /* is_partial= */ false, | ||||||
|  |                 {COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS})); | ||||||
|  |  | ||||||
|  |         // Test tool call with content after | ||||||
|  |         common_chat_msg msg_content_after_tool; | ||||||
|  |         msg_content_after_tool.role = "assistant"; | ||||||
|  |         msg_content_after_tool.content = "Here's the result."; | ||||||
|  |         msg_content_after_tool.tool_calls.push_back({"get_weather", "{\"location\":\"Paris\"}", ""}); | ||||||
|  |         assert_msg_equals( | ||||||
|  |             msg_content_after_tool, | ||||||
|  |             common_chat_parse( | ||||||
|  |                 "<|tool_call_start|>[{\"name\": \"get_weather\", \"arguments\": {\"location\": \"Paris\"}}]<|tool_call_end|>Here's the result.", | ||||||
|  |                 /* is_partial= */ false, | ||||||
|  |                 {COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS})); | ||||||
|  |  | ||||||
|  |         // Test tool call with newlines (common in LLM output) | ||||||
|  |         common_chat_msg msg_tool_call_newlines; | ||||||
|  |         msg_tool_call_newlines.role = "assistant"; | ||||||
|  |         msg_tool_call_newlines.tool_calls.push_back({"get_current_time", "{\"location\":\"Paris\"}", ""}); | ||||||
|  |         assert_msg_equals( | ||||||
|  |             msg_tool_call_newlines, | ||||||
|  |             common_chat_parse( | ||||||
|  |                 "<|tool_call_start|>[{\n    \"name\": \"get_current_time\",\n    \"arguments\": {\n        \"location\": \"Paris\"\n    }\n}]<|tool_call_end|>", | ||||||
|  |                 /* is_partial= */ false, | ||||||
|  |                 {COMMON_CHAT_FORMAT_LFM2_WITH_JSON_TOOLS})); | ||||||
|  |  | ||||||
|  |         // Note: LFM2 uses JSON format for tool calls: [{"name": "...", "arguments": {...}}] | ||||||
|  |         // Unlike other formats, LFM2 template does not render tool calls in conversation history, | ||||||
|  |         // so we don't use test_templates() for tool call generation. Instead, the parsing tests | ||||||
|  |         // above verify edge cases and format variations for the tool call output format. | ||||||
|  |     } | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Yuri Khrustalev
					Yuri Khrustalev