diff --git a/common/chat.cpp b/common/chat.cpp index ee2e777af2..0461c00234 100644 --- a/common/chat.cpp +++ b/common/chat.cpp @@ -6,6 +6,7 @@ #include "common.h" #include "ggml.h" #include "json-schema-to-grammar.h" +#include "json.h" #include "log.h" #include "jinja/value.h" @@ -972,7 +973,7 @@ static std::string common_chat_template_direct_apply_impl( jinja::caps_apply_reasoning_effort(ctx, reasoning_effort); } - jinja::global_from_json(ctx, inp, inputs.mark_input); + jinja::global_from_json(ctx, common_json_ref_from_raw(inp), inputs.mark_input); // render jinja::runtime runtime(ctx); diff --git a/common/jinja/README.md b/common/jinja/README.md index 8291240767..5b97fc92c5 100644 --- a/common/jinja/README.md +++ b/common/jinja/README.md @@ -7,7 +7,7 @@ The implementation can be found in the `common/jinja` directory. ## Key Features - Input marking: security against special token injection -- Decoupled from `nlohmann::json`: this dependency is only used for JSON-to-internal type translation and is completely optional +- Decoupled from the JSON library: `common_json` is only used for JSON-to-internal type translation and is completely optional - Minimal primitive types: int, float, bool, string, array, object, none, undefined - Detailed logging: allow source tracing on error - Clean architecture: workarounds are applied to input data before entering the runtime (see `common/chat.cpp`) diff --git a/common/jinja/caps.cpp b/common/jinja/caps.cpp index 6e3a1e9b29..53965d9903 100644 --- a/common/jinja/caps.cpp +++ b/common/jinja/caps.cpp @@ -4,14 +4,14 @@ // note: the json dependency is only for defining input in a convenient way // we can remove it in the future when we figure out a better way to define inputs using jinja::value -#include +#include "json.h" #include #include #define FILENAME "jinja-caps" -using json = nlohmann::ordered_json; +using json = common_json; namespace jinja { @@ -370,7 +370,7 @@ caps caps_get(jinja::program & prog) { caps_try_execute( prog, [&]() { - json args = json(R"({"arg": "value"})"); + json args = json::make(R"({"arg": "value"})"); if (result.supports_object_arguments) { args = json{{"arg", "value"}}; } diff --git a/common/jinja/value.cpp b/common/jinja/value.cpp index 870596d617..cd92a804bc 100644 --- a/common/jinja/value.cpp +++ b/common/jinja/value.cpp @@ -3,7 +3,7 @@ #include "value.h" // for converting from JSON to jinja values -#include +#include "json.h" #include #include @@ -1355,7 +1355,7 @@ const func_builtins & value_undefined_t::get_builtins() const { ////////////////////////////////// -static value from_json(const nlohmann::ordered_json & j, bool mark_input) { +static value from_json(const common_json_ref & j, bool mark_input) { if (j.is_null()) { return mk_val(); } else if (j.is_boolean()) { @@ -1378,8 +1378,8 @@ static value from_json(const nlohmann::ordered_json & j, bool mark_input) { return arr; } else if (j.is_object()) { auto obj = mk_val(); - for (auto it = j.begin(); it != j.end(); ++it) { - obj->insert(it.key(), from_json(it.value(), mark_input)); + for (const auto & [key, val] : j.items()) { + obj->insert(key, from_json(val, mark_input)); } return obj; } else { @@ -1451,18 +1451,20 @@ bool value_compare(const value & a, const value & b, value_compare_op op) { return result; } -template<> -void global_from_json(context & ctx, const nlohmann::ordered_json & json_obj, bool mark_input) { - // printf("global_from_json: %s\n" , json_obj.dump(2).c_str()); +template +void global_from_json(context & ctx, const T_JSON & json_obj, bool mark_input) { if (json_obj.is_null() || !json_obj.is_object()) { throw std::runtime_error("global_from_json: input JSON value must be an object"); } - for (auto it = json_obj.begin(); it != json_obj.end(); ++it) { - JJ_DEBUG("global_from_json: setting key '%s'", it.key().c_str()); - ctx.set_val(it.key(), from_json(it.value(), mark_input)); + for (const auto & [key, val] : json_obj.items()) { + JJ_DEBUG("global_from_json: setting key '%s'", key.c_str()); + ctx.set_val(key, from_json(val, mark_input)); } } +template void global_from_json (context &, const common_json &, bool); +template void global_from_json(context &, const common_json_ref &, bool); + // recursively convert value to JSON string // TODO: avoid circular references static void value_to_json_internal(std::ostringstream & oss, const value & val, int curr_lvl, int indent, const std::string_view item_sep, const std::string_view key_sep) { diff --git a/common/jinja/value.h b/common/jinja/value.h index 5cf85e4f54..bf81cdc4a3 100644 --- a/common/jinja/value.h +++ b/common/jinja/value.h @@ -86,7 +86,7 @@ struct context; // forward declaration // marking input can be useful for tracking data provenance // and preventing template injection attacks // -// Note: T_JSON can be nlohmann::ordered_json +// Note: T_JSON can be common_json or common_json_ref template void global_from_json(context & ctx, const T_JSON & json_obj, bool mark_input); diff --git a/common/json.cpp b/common/json.cpp index aada499fe6..9f88b71708 100644 --- a/common/json.cpp +++ b/common/json.cpp @@ -15,10 +15,6 @@ static ordered_json & as_json(common_json_node * node) { return *reinterpret_cast(node); } -static const ordered_json & as_json(const common_json_node * node) { - return *reinterpret_cast(node); -} - static common_json_node * as_node(ordered_json * json) { return reinterpret_cast(json); } @@ -59,10 +55,15 @@ template common_json common_json_from_raw(const T & json) { return make_json(json); } +template common_json_ref common_json_ref_from_raw(T & json) { + return common_json_ref(as_node(&json)); +} + // the bridge is usable only for the type below template ordered_json & common_json_raw(common_json_ref &); template const ordered_json & common_json_raw(const common_json_ref &); template common_json common_json_from_raw(const ordered_json &); +template common_json_ref common_json_ref_from_raw(ordered_json &); common_json_value::common_json_value(const char * val) { if (val) { @@ -76,6 +77,9 @@ common_json_value::common_json_value(const char * val) { common_json_value::common_json_value(const common_json & val) : type(VAL_JSON), val_json(std::make_shared(val)) {} +common_json_value::common_json_value(std::initializer_list items) : + type(VAL_JSON), val_json(std::make_shared(items)) {} + common_json_value::common_json_value(const common_json_ref & val) : type(VAL_JSON), val_json(std::make_shared(make_json(as_json(val.get_node())))) {} @@ -86,6 +90,7 @@ bool common_json_ref::is_string() const { return as_json(node).is_string bool common_json_ref::is_boolean() const { return as_json(node).is_boolean(); } bool common_json_ref::is_number() const { return as_json(node).is_number(); } bool common_json_ref::is_number_integer() const { return as_json(node).is_number_integer(); } +bool common_json_ref::is_number_float() const { return as_json(node).is_number_float(); } bool common_json_ref::empty() const { return as_json(node).empty(); } size_t common_json_ref::size() const { return as_json(node).size(); } @@ -199,6 +204,16 @@ common_json common_json::array() { return make_json(ordered_json::array()); } +common_json common_json::array(std::initializer_list vals) { + ordered_json out = ordered_json::array(); + + for (const auto & val : vals) { + out.push_back(to_json(val)); + } + + return make_json(out); +} + common_json common_json::object() { return common_json(); } diff --git a/common/json.h b/common/json.h index ab4163790e..3b37973dd0 100644 --- a/common/json.h +++ b/common/json.h @@ -1,6 +1,7 @@ #pragma once // JSON object, it works without the need to include a JSON library header +// the underlay library is pimpl, it should never be exposed here // note: object keys keep the order in which they are added #include @@ -15,6 +16,9 @@ class common_json; class common_json_ref; +// common_json_value holds a list of these, and each of them holds a value, so one must come first +struct common_json_item; + // one value of the backing library, only json.cpp knows what it is struct common_json_node; @@ -57,6 +61,9 @@ struct common_json_value { common_json_value(const common_json & val); common_json_value(const common_json_ref & val); + // nested object, e.g. {"fn", {{"name", "x"}}} + common_json_value(std::initializer_list items); + template ::value && !std::is_same::value, int>::type = 0> common_json_value(T val) : type(std::is_signed::value ? VAL_INT : VAL_UINT) { if (std::is_signed::value) { @@ -77,6 +84,10 @@ struct common_json_item { template common_json_item(std::string key, T && val) : key(std::move(key)), val(std::forward(val)) {} + + // a braced list cannot deduce T, so it needs its own overload + common_json_item(std::string key, std::initializer_list items) : + key(std::move(key)), val(items) {} }; // view to a value owned by a common_json, it goes stale if the owner gets a new key @@ -96,6 +107,7 @@ class common_json_ref { bool is_boolean() const; bool is_number() const; bool is_number_integer() const; + bool is_number_float() const; bool empty() const; size_t size() const; @@ -170,9 +182,9 @@ class common_json_ref { std::pair operator*() const; iterator & operator++() { - idx++; - return *this; - } + idx++; + return *this; + } bool operator!=(const iterator & other) const { return idx != other.idx; } @@ -215,6 +227,7 @@ class common_json : public common_json_ref { static common_json parse(const std::string & text); static common_json array(); + static common_json array(std::initializer_list vals); static common_json object(); // holds a single value, e.g. make("abc").dump() gives "\"abc\"" @@ -232,3 +245,6 @@ template T & common_json_raw(common_json_ref & json); template const T & common_json_raw(const common_json_ref & json); template common_json common_json_from_raw(const T & json); + +// view over a value of the backing library, it does not copy +template common_json_ref common_json_ref_from_raw(T & json);