diff --git a/common/jinja/jinja-value.cpp b/common/jinja/jinja-value.cpp index f382a64a86..70cca62cff 100644 --- a/common/jinja/jinja-value.cpp +++ b/common/jinja/jinja-value.cpp @@ -385,14 +385,30 @@ const func_builtins & value_string_t::get_builtins() const { return input; } }}, + {"slice", [](const func_args & args) -> value { + auto & input = args.args[0]; + if (!is_val(input)) { + throw raised_exception("slice() first argument must be a string"); + } + if (args.args.size() < 1 || args.args.size() > 4) { + throw raised_exception("slice() takes between 1 and 4 arguments"); + } + int64_t start = is_val(args.args[1]) ? args.args[1]->as_int() : 0; + int64_t stop = is_val(args.args[2]) ? args.args[2]->as_int() : -1; + int64_t step = is_val(args.args[3]) ? args.args[3]->as_int() : 1; + if (step == 0) { + throw raised_exception("slice step cannot be zero"); + } + auto sliced = slice(input->as_string().str(), start, stop, step); + auto res = mk_val(sliced); + res->val_str.mark_input_based_on(input->as_string()); + return res; + }}, {"indent", [](const func_args &) -> value { - throw std::runtime_error("indent builtin not implemented"); + throw std::runtime_error("String indent builtin not implemented"); }}, {"join", [](const func_args &) -> value { - throw std::runtime_error("join builtin not implemented"); - }}, - {"slice", [](const func_args &) -> value { - throw std::runtime_error("slice builtin not implemented"); + throw std::runtime_error("String join builtin not implemented"); }}, }; return builtins; @@ -635,15 +651,21 @@ const func_builtins & value_object_t::get_builtins() const { const func_builtins & value_null_t::get_builtins() const { static const func_builtins builtins = { - {"list", [](const func_args &) -> value { + {"list", [](const func_args & args) -> value { // fix for meetkai-functionary-medium-v3.1.jinja - // TODO: hide under a flag? - return mk_val(); + if (args.ctx.wrk_around.none_has_builtins) { + return mk_val(); + } else { + throw raised_exception("'list' builtin not supported for none type"); + } }}, - {"selectattr", [](const func_args &) -> value { + {"selectattr", [](const func_args & args) -> value { // fix for meetkai-functionary-medium-v3.1.jinja - // TODO: hide under a flag? - return mk_val(); + if (args.ctx.wrk_around.none_has_builtins) { + return mk_val(); + } else { + throw raised_exception("'selectattr' builtin not supported for none type"); + } }}, }; return builtins; diff --git a/common/jinja/jinja-vm.cpp b/common/jinja/jinja-vm.cpp index 0211ef9013..94ee370029 100644 --- a/common/jinja/jinja-vm.cpp +++ b/common/jinja/jinja-vm.cpp @@ -109,6 +109,15 @@ value binary_expression::execute_impl(context & ctx) { // Special case: `anything in undefined` is `false` and `anything not in undefined` is `true` return mk_val(op.value == "not in"); } + if (ctx.wrk_around.string_plus_undefined_is_string && (op.value == "+" || op.value == "~")) { + JJ_DEBUG("%s", "Workaround: treating undefined as empty string for string concatenation"); + auto left_str = left_val->is_undefined() ? string() : left_val->as_string(); + auto right_str = right_val->is_undefined() ? string() : right_val->as_string(); + auto output = left_str.append(right_str); + auto res = mk_val(); + res->val_str = std::move(output); + return res; + } throw std::runtime_error("Cannot perform operation " + op.value + " on undefined values"); } else if (is_val(left_val) || is_val(right_val)) { throw std::runtime_error("Cannot perform operation on null values"); @@ -628,9 +637,12 @@ value member_expression::execute_impl(context & ctx) { } else if (is_val(object) || is_val(object)) { if (is_val(property)) { int64_t index = property->as_int(); - JJ_DEBUG("Accessing %s index %lld", is_val(object) ? "array" : "string", index); + JJ_DEBUG("Accessing %s index %lld", object->type().c_str(), index); if (is_val(object)) { auto & arr = object->as_array(); + if (index < 0) { + index += static_cast(arr.size()); + } if (index >= 0 && index < static_cast(arr.size())) { val = arr[index]; } diff --git a/common/jinja/jinja-vm.h b/common/jinja/jinja-vm.h index 596f325194..045d45d980 100644 --- a/common/jinja/jinja-vm.h +++ b/common/jinja/jinja-vm.h @@ -2,6 +2,7 @@ #include "jinja-lexer.h" #include "jinja-value.h" +#include "jinja-workaround.h" #include #include @@ -52,6 +53,8 @@ struct context { std::time_t current_time; // for functions that need current time + workarounds wrk_around; // workarounds for non-standard jinja behavior + context() { var["true"] = mk_val(true); var["false"] = mk_val(false); diff --git a/common/jinja/jinja-workaround.h b/common/jinja/jinja-workaround.h new file mode 100644 index 0000000000..766132c0ca --- /dev/null +++ b/common/jinja/jinja-workaround.h @@ -0,0 +1,20 @@ +#pragma once + +#include "jinja-value.h" + +#include +#include + +namespace jinja { + +// containing workarounds for Jinja templates that rely on non-standard behavior + +struct workarounds { + // meetkai-functionary-medium-v3.1.jinja call filter on None type + bool none_has_builtins = true; + + // Olmo calls operation + between string and undefined + bool string_plus_undefined_is_string = true; +}; + +} // namespace jinja diff --git a/tests/test-chat-jinja.cpp b/tests/test-chat-jinja.cpp index 72f3ee9822..61ce80d8ac 100644 --- a/tests/test-chat-jinja.cpp +++ b/tests/test-chat-jinja.cpp @@ -28,11 +28,32 @@ int main(void) { std::vector failed_tests; + auto is_ignored_file = [](const std::string & filename) -> bool { + std::vector ignored_files = { + "Apriel-", + "Olmo-3-7B-Instruct-Heretic-GGUF", + }; + for (const auto & ignored : ignored_files) { + if (filename.find(ignored) != std::string::npos) { + return true; + } + } + return false; + }; + // list all files in models/templates/ and run each size_t test_count = 0; - std::string dir_path = "models/templates/"; + size_t skip_count = 0; + //std::string dir_path = "models/templates/"; + std::string dir_path = "../test-jinja/templates/"; for (const auto & entry : std::filesystem::directory_iterator(dir_path)) { if (entry.is_regular_file()) { + if (is_ignored_file(entry.path().filename().string())) { + std::cout << "=== SKIPPING TEMPLATE FILE: " << entry.path().string() << " ===\n"; + skip_count++; + continue; + } + test_count++; std::cout << "\n\n=== RUNNING TEMPLATE FILE: " << entry.path().string() << " ===\n"; std::ifstream infile(entry.path()); @@ -43,6 +64,7 @@ int main(void) { std::cout << "Exception: " << e.what() << "\n"; std::cout << "=== ERROR WITH TEMPLATE FILE: " << entry.path().string() << " ===\n"; failed_tests.push_back(entry.path().string()); + exit(1); } } } @@ -50,6 +72,7 @@ int main(void) { std::cout << "\n\n=== TEST SUMMARY ===\n"; std::cout << "Total tests run: " << test_count << "\n"; std::cout << "Total failed tests: " << failed_tests.size() << "\n"; + std::cout << "Total skipped tests: " << skip_count << "\n"; for (const auto & test : failed_tests) { std::cout << "FAILED TEST: " << test << "\n"; } @@ -92,6 +115,7 @@ void run(std::string contents) { ], "bos_token": "", "eos_token": "", + "tools": [], "functions": "", "datetime": "" })";