char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Sigbjørn Skjæretcaps : recheck typed content if template checks for string (#28511)5202104b5

char/aliceai-t5-moe
20.1 KiB573 linesraw
1#include "value.h"
2#include "runtime.h"
3#include "caps.h"
4
5// note: the json dependency is only for defining input in a convenient way
6// we can remove it in the future when we figure out a better way to define inputs using jinja::value
7#include "json.h"
8
9#include <functional>
10#include <sstream>
11
12#define FILENAME "jinja-caps"
13
14using json = common_json;
15
16namespace jinja {
17
18using caps_json_fn = std::function<json()>;
19using caps_ctx_fn = std::function<void(context &)>;
20using caps_analyze_fn = std::function<void(context &, bool, value &, value &, const std::string &)>;
21
22void caps_apply_preserve_reasoning(jinja::context & ctx, bool enabled) {
23    ctx.set_val("preserve_thinking",         mk_val<value_bool>(enabled));
24    ctx.set_val("clear_thinking",            mk_val<value_bool>(!enabled));
25    ctx.set_val("truncate_history_thinking", mk_val<value_bool>(!enabled));
26    ctx.set_val("drop_thinking",             mk_val<value_bool>(!enabled));
27}
28
29void caps_apply_reasoning_effort(jinja::context & ctx, const std::string & effort) {
30    value var = mk_val<value_string>(effort); // bind to the same value for stats
31    ctx.set_val("reasoning_effort",   var);
32    ctx.set_val("reasoning_strength", var);
33}
34
35static void caps_try_execute(jinja::program & prog,
36                             const caps_json_fn & messages_fn,
37                             const caps_ctx_fn & ctx_fn,
38                             const caps_json_fn & tools_fn,
39                             const caps_analyze_fn & analyze_fn) {
40    context ctx;
41    ctx.is_get_stats = true;
42    jinja::global_from_json(ctx, json{
43        {"messages", messages_fn()},
44        {"tools", tools_fn ? tools_fn() : json::array()},
45        {"bos_token", ""},
46        {"eos_token", ""},
47        {"add_generation_prompt", true}
48    }, true);
49
50    if (ctx_fn) {
51        ctx_fn(ctx);
52    }
53
54    auto messages = ctx.get_val("messages");
55    auto tools = ctx.get_val("tools");
56
57    bool success = false;
58    std::string result;
59    try {
60        jinja::runtime runtime(ctx);
61        auto results = runtime.execute(prog);
62        auto parts = jinja::runtime::gather_string_parts(results);
63        result = parts->as_string().str();
64        success = true;
65    } catch (const std::exception & e) {
66        JJ_DEBUG("Exception during execution: %s", e.what());
67        result = "";
68        // ignore exceptions during capability analysis
69    }
70
71    analyze_fn(ctx, success, messages, tools, result);
72}
73
74// for debugging only
75static void caps_print_stats(value & v, const std::string & path) {
76    std::string ops;
77    for (const auto & name : v->stats.ops) {
78        ops += name + " ";
79    }
80    JJ_DEBUG("Value %s, type: %s %s, ops: %s",
81                path.c_str(),
82                v->type().c_str(),
83                v->stats.used ? "(used)" : "",
84                ops.c_str());
85}
86
87std::map<std::string, bool> caps::to_map() const {
88    return {
89        {"supports_string_content", supports_string_content},
90        {"supports_typed_content", supports_typed_content},
91        {"supports_tools", supports_tools},
92        {"supports_tool_calls", supports_tool_calls},
93        {"supports_parallel_tool_calls", supports_parallel_tool_calls},
94        {"supports_system_role", supports_system_role},
95        {"supports_preserve_reasoning", supports_preserve_reasoning},
96        {"supports_reasoning_effort", supports_reasoning_effort},
97        {"supports_object_arguments", supports_object_arguments},
98    };
99}
100
101std::string caps::to_string() const {
102    std::ostringstream ss;
103    ss << "Caps(\n";
104    for (const auto & [key, value] : to_map()) {
105        ss << "  " << key << "=" << (value ? "true" : "false") << "\n";
106    }
107    ss << ")";
108    return ss.str();
109}
110
111caps caps_get(jinja::program & prog) {
112    caps result;
113
114    static const auto has_op = [](value & v, const std::string & op_name) {
115        return v->stats.ops.find(op_name) != v->stats.ops.end();
116    };
117
118    JJ_DEBUG("%s\n", ">>> Running capability check: typed content");
119
120    bool checks_for_string = false;
121    static const std::string content_marker = "STRING_MARKER";
122
123    // case: typed content support
124    caps_try_execute(
125        prog,
126        [&]() {
127            // messages
128            return json::array({
129                {
130                    {"role", "user"},
131                    {"content", content_marker}
132                }
133            });
134        },
135        nullptr, // ctx_fn
136        nullptr, // tools_fn
137        [&](context &, bool success, value & messages, value &, const std::string & rendered) {
138            auto & content = messages->at(0)->at("content");
139            caps_print_stats(content, "messages[0].content");
140            if (has_op(content, "test_is_string")) {
141                // checked if content is string
142                checks_for_string = true;
143            }
144            bool used_as_array = has_op(content, "selectattr") || has_op(content, "array_access");
145            if (used_as_array) {
146                // accessed as an array
147                result.supports_typed_content = true;
148            }
149            if (!success) {
150                // failed to execute with content as string
151                result.supports_string_content = false;
152            } else if (used_as_array && rendered.find(content_marker) == std::string::npos) {
153                // edge case: string may be accessed for checking, but does not appear in the output
154                result.supports_string_content = false;
155            }
156        }
157    );
158
159    if (checks_for_string) {
160        caps_try_execute(
161            prog,
162            [&]() {
163                // messages
164                return json::array({
165                    {
166                        {"role", "user"},
167                        {"content", json::array({
168                        })}
169                    }
170                });
171            },
172            nullptr, // ctx_fn
173            nullptr, // tools_fn
174            [&](context &, bool success, value & messages, value &, const std::string &) {
175                auto & content = messages->at(0)->at("content");
176                caps_print_stats(content, "messages[0].content");
177                bool used_as_array = has_op(content, "selectattr") || has_op(content, "array_access");
178                if (used_as_array && success) {
179                    // accessed as an array
180                    result.supports_typed_content = true;
181                }
182            }
183        );
184    }
185
186    JJ_DEBUG("%s\n", ">>> Running capability check: system prompt");
187
188    // case: system prompt support
189    caps_try_execute(
190        prog,
191        [&]() {
192            // messages
193            return json::array({
194                {
195                    {"role", "system"},
196                    {"content", "System message"}
197                },
198                {
199                    {"role", "user"},
200                    {"content", "User message"}
201                },
202            });
203        },
204        nullptr, // ctx_fn
205        nullptr, // tools_fn
206        [&](context &, bool, value & messages, value &, const std::string &) {
207            auto & content = messages->at(0)->at("content");
208            caps_print_stats(content, "messages[0].content");
209            if (!content->stats.used) {
210                result.supports_system_role = false;
211            }
212        }
213    );
214
215    JJ_DEBUG("%s\n", ">>> Running capability check: single tool with object arguments support");
216
217    // case: tools support: single call with object arguments
218    caps_try_execute(
219        prog,
220        [&]() {
221            // messages
222            return json::array({
223                {
224                    {"role", "user"},
225                    {"content", "User message"},
226                },
227                {
228                    {"role", "assistant"},
229                    {"content", ""}, // Some templates expect content to be empty with tool calls
230                    {"tool_calls", json::array({
231                        {
232                            {"id", "call00001"},
233                            {"type", "function"},
234                            {"function", {
235                                {"name", "tool1"},
236                                {"arguments", {
237                                    {"arg", "value"}
238                                }}
239                            }}
240                        }
241                    })}
242                },
243                {
244                    {"role", "tool"},
245                    {"content", "Tool response"},
246                    {"tool_call_id", "call00001"}
247                },
248                {
249                    {"role", "assistant"},
250                    {"content", "The tool response was 'tool response'"}
251                },
252                {
253                    {"role", "user"},
254                    {"content", "User message"},
255                },
256            });
257        },
258        nullptr, // ctx_fn
259        [&]() {
260            // tools
261            return json::array({
262                {
263                    {"name", "tool"},
264                    {"type", "function"},
265                    {"function", {
266                        {"name", "tool1"},
267                        {"description", "Tool description"},
268                        {"parameters", {
269                            {"type", "object"},
270                            {"properties", {
271                                {"arg", {
272                                    {"type", "string"},
273                                    {"description", "Arg description"},
274                                }},
275                            }},
276                            {"required", json::array({ "arg" })},
277                        }},
278                    }},
279                },
280            });
281        },
282        [&](context &, bool success, value & messages, value & tools, const std::string &) {
283            if (!success) {
284                return; // Nothing can be inferred
285            }
286
287            auto & tool_name = tools->at(0)->at("function")->at("name");
288            caps_print_stats(tool_name, "tools[0].function.name");
289            caps_print_stats(tools, "tools");
290            if (!tool_name->stats.used) {
291                result.supports_tools = false;
292            }
293
294            auto & tool_calls = messages->at(1)->at("tool_calls");;
295            caps_print_stats(tool_calls, "messages[1].tool_calls");
296            if (!tool_calls->stats.used) {
297                result.supports_tool_calls = false;
298                return;
299            }
300
301            auto & tool_arg = tool_calls->at(0)->at("function")->at("arguments")->at("arg");
302            caps_print_stats(tool_arg, "messages[1].tool_calls[0].function.arguments.arg");
303            if (tool_arg->stats.used) {
304                result.supports_object_arguments = true;
305            }
306        }
307    );
308
309    if (!result.supports_object_arguments) {
310        JJ_DEBUG("%s\n", ">>> Running capability check: single tool with string arguments support");
311
312        // case: tools support: single call with string arguments
313        caps_try_execute(
314            prog,
315            [&]() {
316                // messages
317                return json::array({
318                    {
319                        {"role", "user"},
320                        {"content", "User message"},
321                    },
322                    {
323                        {"role", "assistant"},
324                        {"content", ""}, // Some templates expect content to be empty with tool calls
325                        {"tool_calls", json::array({
326                            {
327                                {"id", "call00001"},
328                                {"type", "function"},
329                                {"function", {
330                                    {"name", "tool1"},
331                                    {"arguments", R"({"arg": "value"})"}
332                                }}
333                            }
334                        })}
335                    },
336                    {
337                        {"role", "tool"},
338                        {"content", "Tool response"},
339                        {"tool_call_id", "call00001"}
340                    },
341                    {
342                        {"role", "assistant"},
343                        {"content", "The tool response was 'tool response'"}
344                    },
345                    {
346                        {"role", "user"},
347                        {"content", "User message"},
348                    },
349                });
350            },
351            nullptr, // ctx_fn
352            [&]() {
353                // tools
354                return json::array({
355                    {
356                        {"name", "tool"},
357                        {"type", "function"},
358                        {"function", {
359                            {"name", "tool1"},
360                            {"description", "Tool description"},
361                            {"parameters", {
362                                {"type", "object"},
363                                {"properties", {
364                                    {"arg", {
365                                        {"type", "string"},
366                                        {"description", "Arg description"},
367                                    }},
368                                }},
369                                {"required", json::array({ "arg" })},
370                            }},
371                        }},
372                    },
373                });
374            },
375            [&](context &, bool success, value & messages, value & tools, const std::string &) {
376                if (!success) {
377                    result.supports_tool_calls = false;
378                    result.supports_tools = false;
379                    return;
380                }
381
382                auto & tool_name = tools->at(0)->at("function")->at("name");
383                caps_print_stats(tool_name, "tools[0].function.name");
384                caps_print_stats(tools, "tools");
385                if (!tool_name->stats.used) {
386                    result.supports_tools = false;
387                }
388
389                auto & tool_calls = messages->at(1)->at("tool_calls");
390                caps_print_stats(tool_calls, "messages[1].tool_calls");
391                if (!tool_calls->stats.used) {
392                    result.supports_tool_calls = false;
393                    return;
394                }
395            }
396        );
397    }
398
399    JJ_DEBUG("%s\n", ">>> Running capability check: parallel tool support");
400
401    // case: tools support: parallel calls
402    caps_try_execute(
403        prog,
404        [&]() {
405            json args = json(R"({"arg": "value"})");
406            if (result.supports_object_arguments) {
407                args = json{{"arg", "value"}};
408            }
409
410            // messages
411            return json::array({
412                {
413                    {"role", "user"},
414                    {"content", "User message"},
415                },
416                {
417                    {"role", "assistant"},
418                    {"content", ""}, // Some templates expect content to be empty with tool calls
419                    {"tool_calls", json::array({
420                        {
421                            {"id", "call00001"},
422                            {"type", "function"},
423                            {"function", {
424                                {"name", "tool1"},
425                                {"arguments", args}
426                            }}
427                        },
428                        {
429                            {"id", "call00002"},
430                            {"type", "function"},
431                            {"function", {
432                                {"name", "tool1"},
433                                {"arguments", args}
434                            }}
435                        }
436                    })}
437                },
438                {
439                    {"role", "tool"},
440                    {"content", "Tool response"},
441                    {"tool_call_id", "call00001"}
442                },
443                {
444                    {"role", "assistant"},
445                    {"content", "The tool response was 'tool response'"}
446                },
447                {
448                    {"role", "user"},
449                    {"content", "User message"},
450                },
451            });
452        },
453        nullptr, // ctx_fn
454        [&]() {
455            // tools
456            return json::array({
457                {
458                    {"name", "tool"},
459                    {"type", "function"},
460                    {"function", {
461                        {"name", "tool1"},
462                        {"description", "Tool description"},
463                        {"parameters", {
464                            {"type", "object"},
465                            {"properties", {
466                                {"arg", {
467                                    {"type", "string"},
468                                    {"description", "Arg description"},
469                                }},
470                            }},
471                            {"required", json::array({ "arg" })},
472                        }},
473                    }},
474                },
475            });
476        },
477        [&](context &, bool success, value & messages, value &, const std::string &) {
478            if (!success) {
479                result.supports_parallel_tool_calls = false;
480                return;
481            }
482
483            auto & tool_calls = messages->at(1)->at("tool_calls");
484            caps_print_stats(tool_calls, "messages[1].tool_calls");
485
486            // check for second tool call usage
487            auto & tool_call_1 = tool_calls->at(1)->at("function");
488            caps_print_stats(tool_call_1, "messages[1].tool_calls[1].function");
489            if (!tool_call_1->stats.used) {
490                result.supports_parallel_tool_calls = false;
491            }
492        }
493    );
494
495    JJ_DEBUG("%s\n", ">>> Running capability check: preserve reasoning");
496
497    // case: preserve reasoning content in chat history
498    const std::string reasoning_placeholder = "<REASONING_CONTENT_PLACEHOLDER>";
499    caps_try_execute(
500        prog,
501        [&]() {
502            // messages
503            return json::array({
504                {
505                    {"role", "user"},
506                    {"content", "User message"}
507                },
508                {
509                    {"role", "assistant"},
510                    {"content", "Assistant message"},
511                    // check of reasoning_content deeper in the history, not just the last assistant message
512                    {"reasoning_content", reasoning_placeholder}
513                },
514                {
515                    {"role", "user"},
516                    {"content", "User message"}
517                },
518                {
519                    {"role", "assistant"},
520                    {"content", "Assistant message"},
521                    {"reasoning_content", "Reasoning content"}
522                },
523                {
524                    {"role", "user"},
525                    {"content", "User message"}
526                },
527            });
528        },
529        [&](context & ctx) {
530            ctx.set_val("enable_thinking", mk_val<value_bool>(true));
531            caps_apply_preserve_reasoning(ctx, true);
532        },
533        nullptr, // tools_fn
534        [&](context &, bool, value &, value &, const std::string & output) {
535            // note: we cannot use stats here because the reasoning_content may be used for "if" condition test, but not actually outputted in the final result
536            if (output.find(reasoning_placeholder) != std::string::npos) {
537                result.supports_preserve_reasoning = true;
538            }
539        }
540    );
541
542    JJ_DEBUG("%s\n", ">>> Running capability check: reasoning effort");
543
544    // case: reasoning effort level
545    caps_try_execute(
546        prog,
547        [&]() {
548            // messages
549            return json::array({
550                {
551                    {"role", "user"},
552                    {"content", "User message"}
553                },
554            });
555        },
556        [&](context & ctx) {
557            ctx.set_val("enable_thinking", mk_val<value_bool>(true));
558            caps_apply_reasoning_effort(ctx, "low");
559        },
560        nullptr, // tools_fn
561        [&](context & ctx, bool, value &, value &, const std::string &) {
562            value effort = ctx.get_val("reasoning_effort");
563            caps_print_stats(effort, "reasoning_effort");
564            result.supports_reasoning_effort = effort->stats.used;
565        }
566    );
567
568    JJ_DEBUG("%s\n", result.to_string().c_str());
569
570    return result;
571}
572
573} // namespace jinja