mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-17 20:31:47 +02:00
* common : implement common_schema types
* common : implement a json schema optimizer
* common : reduce optimizations
* common : refactor json-schema-to-grammar to use common_schema
* common : use common_trie
* common/schema : implement type/kind resolution
* cont : cleanup
* cont : remove common_chat_tool_parameters
* cont : simplify schema resolution
* cont : pass common_schema through the json-schema-to-grammar builder
* cont : cleanup
* cont : move enums under common_schema and add type enum
* cont : reduce test cases
* cont : clean up
* cont : clean up
* refactor : rename common_schema_parse to common_schema_from_json
* tests : fix gcc dangling-reference warning in test-json-schema
* tests : take the schema label as const char * to satisfy gcc dangling-reference
* refactor : rename common_schema_builder parse_* methods to build_*
* cont : fix may_be_string
* cont : properly handle empty tool parameters
* cont : add tests for empty $ref
* cont : remove dead code
* cont : update docs
* cont : make "{}" mean any object for json_object as well
* cont : restore (min|max)Length to imply string type
* cont : rename common_schema to common_chat_schema
230 lines
10 KiB
C++
230 lines
10 KiB
C++
#include "parsers.h"
|
|
|
|
common_chat_params common_chat_params_init_minimax_m3(const common_chat_template & tmpl,
|
|
const autoparser::generation_params & inputs) {
|
|
common_chat_params data;
|
|
|
|
data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs);
|
|
data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs);
|
|
data.format = COMMON_CHAT_FORMAT_PEG_MINIMAX_M3;
|
|
data.supports_thinking = true;
|
|
data.thinking_start_tag = "<mm:think>";
|
|
data.thinking_end_tags = {"</mm:think>"};
|
|
|
|
// M3 prefixes every tool tag with the namespace token "]<]minimax[>[";
|
|
// params use the parameter name as the tag (<file_path>...</file_path>).
|
|
const std::string NS = "]<]minimax[>[";
|
|
const std::string THINK_START = "<mm:think>";
|
|
const std::string THINK_END = "</mm:think>";
|
|
const std::string FC_START = NS + "<tool_call>";
|
|
const std::string FC_END = NS + "</tool_call>";
|
|
const std::string INVOKE_END = NS + "</invoke>";
|
|
|
|
data.preserved_tokens = {
|
|
NS,
|
|
"<tool_call>",
|
|
"</tool_call>",
|
|
THINK_START,
|
|
THINK_END,
|
|
};
|
|
|
|
data.message_delimiters = {
|
|
{ COMMON_CHAT_ROLE_ASSISTANT, "]~b]ai" },
|
|
{ COMMON_CHAT_ROLE_USER, "]~b]user" },
|
|
{ COMMON_CHAT_ROLE_TOOL, "]~b]tool" },
|
|
{ COMMON_CHAT_ROLE_SYSTEM, "]~b]developer" },
|
|
{ COMMON_CHAT_ROLE_SYSTEM, "]~b]system" },
|
|
};
|
|
|
|
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
|
|
auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
|
|
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
|
|
auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
|
|
|
|
const std::string GEN_PROMPT = data.generation_prompt;
|
|
|
|
using mm3 = common_chat_peg_minimax_m3_mapper;
|
|
|
|
if (inputs.has_continuation()) {
|
|
const auto & msg = inputs.continue_msg;
|
|
|
|
data.generation_prompt = GEN_PROMPT + THINK_START + msg.reasoning_content;
|
|
if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_CONTENT) {
|
|
data.generation_prompt += THINK_END + msg.render_content();
|
|
}
|
|
|
|
data.prompt += data.generation_prompt;
|
|
}
|
|
|
|
auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
|
|
auto generation_prompt = p.prefix(GEN_PROMPT, THINK_START);
|
|
auto end = p.end();
|
|
|
|
auto reasoning = p.eps();
|
|
if (extract_reasoning) {
|
|
auto block = inputs.enable_thinking
|
|
? p.literal(THINK_START) + p.space() +
|
|
p.ac(p.reasoning(p.until(THINK_END)) + p.literal(THINK_END), THINK_END)
|
|
: p.literal(THINK_START) + p.ac(p.until(THINK_END) + p.literal(THINK_END), THINK_END);
|
|
|
|
// A turn without reasoning is prefixed with a bare </mm:think>, written either by the
|
|
// generation prompt (thinking_mode = "disabled") or by the model itself.
|
|
reasoning = p.optional(p.choice({ block, p.literal(THINK_END) }));
|
|
}
|
|
|
|
if (has_response_format) {
|
|
auto response_format = p.rule("response-format",
|
|
p.literal("```json") + p.space() +
|
|
p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)) +
|
|
p.space() + p.literal("```"));
|
|
return generation_prompt + reasoning + response_format + end;
|
|
}
|
|
|
|
if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
|
|
return generation_prompt + reasoning + p.content(p.rest()) + end;
|
|
}
|
|
|
|
auto tool_choice = p.choice();
|
|
foreach_function(inputs.tools, [&](const json & tool) {
|
|
const auto & function = tool.at("function");
|
|
std::string name = function.at("name");
|
|
auto params = common_chat_tool_parameters(function);
|
|
auto doc = std::make_shared<const common_chat_schema_document>(common_chat_schema_from_json(params));
|
|
|
|
// The template expands argument values recursively in XML (see the to_xml() macro)
|
|
std::function<common_peg_parser(const common_chat_schema &, const std::string &, const std::string &)> value_of;
|
|
std::function<common_peg_parser(const common_chat_schema_object &, const std::string &)> members_of;
|
|
|
|
auto element_of = [&](const std::string & tag, const common_chat_schema & schema, const std::string & rule_name) {
|
|
const std::string close = NS + "</" + tag + ">";
|
|
return p.rule(rule_name,
|
|
p.tool_arg(
|
|
p.tool_arg_open(
|
|
p.literal(NS + "<") +
|
|
p.tool_arg_name(p.literal(tag)) +
|
|
p.literal(">")) +
|
|
value_of(schema, rule_name, close)));
|
|
};
|
|
|
|
value_of = [&](const common_chat_schema & schema,
|
|
const std::string & rule_name,
|
|
const std::string & close) -> common_peg_parser {
|
|
auto close_tag = p.tool_arg_close(p.literal(close));
|
|
|
|
// A string accepts anything, so a union with a string alternative is a string
|
|
if (schema.may_be_string()) {
|
|
return p.ac(p.tool_arg_string_value(p.until(close)) + close_tag, close);
|
|
}
|
|
|
|
if (schema.kind() == common_chat_schema::KIND_ANY_OF) {
|
|
std::vector<common_peg_parser> choices;
|
|
|
|
size_t index = 0;
|
|
for (const auto & alternative : static_cast<const common_chat_schema_any_of &>(schema).children) {
|
|
const std::string alt_name = rule_name + "-" + std::to_string(index++);
|
|
|
|
// There is a risk that this breaks streaming deltas, but that's a risk we
|
|
// assume to provide tool arg streaming.
|
|
choices.push_back(value_of(*alternative, alt_name, close));
|
|
}
|
|
|
|
return p.choice(choices);
|
|
}
|
|
|
|
if (schema.kind() == common_chat_schema::KIND_OBJECT) {
|
|
const auto & object = static_cast<const common_chat_schema_object &>(schema);
|
|
if (!object.properties.empty()) {
|
|
return p.tag(mm3::TOOL_ARG_OBJECT, members_of(object, rule_name)) + p.space() + close_tag;
|
|
}
|
|
}
|
|
|
|
if (schema.kind() == common_chat_schema::KIND_ARRAY) {
|
|
const std::string item_close = NS + "</item>";
|
|
auto item = p.rule(rule_name + "-item",
|
|
p.tag(mm3::TOOL_ARG_ITEM,
|
|
p.literal(NS + "<item>") +
|
|
value_of(*static_cast<const common_chat_schema_array &>(schema).items, rule_name + "-item", item_close)));
|
|
return p.tag(mm3::TOOL_ARG_ARRAY, p.repeat(p.space() + item, 0, -1)) + p.space() + close_tag;
|
|
}
|
|
|
|
return p.tool_arg_json_value(p.schema(p.json(), rule_name + "-schema", doc, schema)) + close_tag;
|
|
};
|
|
|
|
// Required properties in schema order, then any number of optional ones in any order.
|
|
members_of = [&](const common_chat_schema_object & object, const std::string & rule_prefix) -> common_peg_parser {
|
|
std::vector<common_peg_parser> required_elements;
|
|
std::vector<common_peg_parser> optional_elements;
|
|
for (const auto & prop : object.properties) {
|
|
auto element = element_of(prop.name, *prop.schema, rule_prefix + "-" + prop.name);
|
|
(prop.required ? required_elements : optional_elements).push_back(element);
|
|
}
|
|
|
|
common_peg_parser members = p.eps();
|
|
for (size_t i = 0; i < required_elements.size(); i++) {
|
|
if (i > 0) {
|
|
members = members + p.space();
|
|
}
|
|
members = members + required_elements[i];
|
|
}
|
|
|
|
if (!optional_elements.empty()) {
|
|
common_peg_parser any_optional = p.choice();
|
|
for (const auto & element : optional_elements) {
|
|
any_optional |= element;
|
|
}
|
|
members = members + p.repeat(p.space() + any_optional, 0, -1);
|
|
}
|
|
|
|
return members;
|
|
};
|
|
|
|
common_peg_parser invoke_body = p.eps();
|
|
if (doc->root->kind() == common_chat_schema::KIND_OBJECT) {
|
|
invoke_body = members_of(static_cast<const common_chat_schema_object &>(*doc->root), "tool-" + name + "-arg");
|
|
}
|
|
|
|
auto func_parser = p.tool(
|
|
p.tool_open(p.literal(NS + "<invoke name=\"") +
|
|
p.tool_name(p.literal(name)) + p.literal("\">")) +
|
|
p.space() + invoke_body + p.space() +
|
|
p.tool_close(p.literal(INVOKE_END)));
|
|
|
|
tool_choice |= p.rule("tool-" + name, func_parser);
|
|
});
|
|
|
|
auto require_tools = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED;
|
|
|
|
common_peg_parser tool_calls = p.eps();
|
|
if (inputs.parallel_tool_calls) {
|
|
tool_calls = p.trigger_rule("tool-call",
|
|
p.literal(FC_START) + p.space() + tool_choice +
|
|
p.zero_or_more(p.space() + tool_choice) + p.space() + p.literal(FC_END));
|
|
} else {
|
|
tool_calls = p.trigger_rule("tool-call",
|
|
p.literal(FC_START) + p.space() + tool_choice + p.space() + p.literal(FC_END));
|
|
}
|
|
|
|
if (!require_tools) {
|
|
tool_calls = p.optional(tool_calls);
|
|
}
|
|
|
|
auto content_before_tools = p.content(p.until(FC_START));
|
|
return generation_prompt + reasoning + content_before_tools + tool_calls + end;
|
|
});
|
|
|
|
data.parser = parser.save();
|
|
|
|
if (include_grammar) {
|
|
data.grammar_lazy = !(has_response_format || (has_tools && inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED));
|
|
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
|
|
parser.build_grammar(builder, data.grammar_lazy);
|
|
});
|
|
|
|
data.grammar_triggers = {
|
|
{ COMMON_GRAMMAR_TRIGGER_TYPE_WORD, FC_START },
|
|
};
|
|
}
|
|
|
|
return data;
|
|
}
|