char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
5202104b5
char/aliceai-t5-moe
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 ) { 23ctx .set_val ("preserve_thinking" ,mk_val < value_bool > (enabled )); 24ctx .set_val ("clear_thinking" ,mk_val < value_bool > (!enabled )); 25ctx .set_val ("truncate_history_thinking" ,mk_val < value_bool > (!enabled )); 26ctx .set_val ("drop_thinking" ,mk_val < value_bool > (!enabled )); 27} 28 29void caps_apply_reasoning_effort (jinja::context & ctx ,const std::string & effort ) { 30value var = mk_val < value_string > (effort );// bind to the same value for stats 31ctx .set_val ("reasoning_effort" ,var ); 32ctx .set_val ("reasoning_strength" ,var ); 33} 34 35static void caps_try_execute (jinja::program & prog , 36const caps_json_fn & messages_fn , 37const caps_ctx_fn & ctx_fn , 38const caps_json_fn & tools_fn , 39const caps_analyze_fn & analyze_fn ) { 40context ctx ; 41ctx .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 50if (ctx_fn ) { 51ctx_fn (ctx ); 52 } 53 54auto messages = ctx .get_val ("messages" ); 55auto tools = ctx .get_val ("tools" ); 56 57bool success = false; 58 std::string result ; 59try { 60 jinja::runtime runtime (ctx ); 61auto results = runtime .execute (prog ); 62auto parts = jinja::runtime::gather_string_parts (results ); 63result = parts -> as_string ().str (); 64success = true; 65 }catch (const std::exception & e ) { 66JJ_DEBUG ("Exception during execution: %s" ,e .what ()); 67result = "" ; 68// ignore exceptions during capability analysis 69 } 70 71analyze_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 ; 77for (const auto & name :v -> stats .ops ) { 78ops += name + " " ; 79 } 80JJ_DEBUG ("Value %s, type: %s %s, ops: %s" , 81path .c_str (), 82v -> type ().c_str (), 83v -> stats .used ?"(used)" :"" , 84ops .c_str ()); 85} 86 87std::map < std::string ,bool > caps::to_map ()const { 88return { 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 ; 103ss <<"Caps(\n" ; 104for (const auto & [key ,value ] :to_map ()) { 105ss <<" " <<key <<"=" << (value ?"true" :"false" ) <<"\n" ; 106 } 107ss <<")" ; 108return ss .str (); 109} 110 111caps caps_get (jinja::program & prog ) { 112caps result ; 113 114static const auto has_op = [](value & v ,const std::string & op_name ) { 115return v -> stats .ops .find (op_name )!= v -> stats .ops .end (); 116 }; 117 118JJ_DEBUG ("%s\n" ,">>> Running capability check: typed content" ); 119 120bool checks_for_string = false; 121static const std::string content_marker = "STRING_MARKER" ; 122 123// case: typed content support 124caps_try_execute ( 125prog , 126 [& ]() { 127// messages 128return json::array ({ 129 { 130 {"role" ,"user" }, 131 {"content" ,content_marker } 132 } 133 }); 134 }, 135nullptr ,// ctx_fn 136nullptr ,// tools_fn 137 [& ](context & ,bool success ,value & messages ,value & ,const std::string & rendered ) { 138auto & content = messages -> at (0 )-> at ("content" ); 139caps_print_stats (content ,"messages[0].content" ); 140if (has_op (content ,"test_is_string" )) { 141// checked if content is string 142checks_for_string = true; 143 } 144bool used_as_array = has_op (content ,"selectattr" )|| has_op (content ,"array_access" ); 145if (used_as_array ) { 146// accessed as an array 147result .supports_typed_content = true; 148 } 149if (!success ) { 150// failed to execute with content as string 151result .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 154result .supports_string_content = false; 155 } 156 } 157 ); 158 159if (checks_for_string ) { 160caps_try_execute ( 161prog , 162 [& ]() { 163// messages 164return json::array ({ 165 { 166 {"role" ,"user" }, 167 {"content" , json::array ({ 168 })} 169 } 170 }); 171 }, 172nullptr ,// ctx_fn 173nullptr ,// tools_fn 174 [& ](context & ,bool success ,value & messages ,value & ,const std::string & ) { 175auto & content = messages -> at (0 )-> at ("content" ); 176caps_print_stats (content ,"messages[0].content" ); 177bool used_as_array = has_op (content ,"selectattr" )|| has_op (content ,"array_access" ); 178if (used_as_array && success ) { 179// accessed as an array 180result .supports_typed_content = true; 181 } 182 } 183 ); 184 } 185 186JJ_DEBUG ("%s\n" ,">>> Running capability check: system prompt" ); 187 188// case: system prompt support 189caps_try_execute ( 190prog , 191 [& ]() { 192// messages 193return json::array ({ 194 { 195 {"role" ,"system" }, 196 {"content" ,"System message" } 197 }, 198 { 199 {"role" ,"user" }, 200 {"content" ,"User message" } 201 }, 202 }); 203 }, 204nullptr ,// ctx_fn 205nullptr ,// tools_fn 206 [& ](context & ,bool ,value & messages ,value & ,const std::string & ) { 207auto & content = messages -> at (0 )-> at ("content" ); 208caps_print_stats (content ,"messages[0].content" ); 209if (!content -> stats .used ) { 210result .supports_system_role = false; 211 } 212 } 213 ); 214 215JJ_DEBUG ("%s\n" ,">>> Running capability check: single tool with object arguments support" ); 216 217// case: tools support: single call with object arguments 218caps_try_execute ( 219prog , 220 [& ]() { 221// messages 222return 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 }, 258nullptr ,// ctx_fn 259 [& ]() { 260// tools 261return 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 & ) { 283if (!success ) { 284return ;// Nothing can be inferred 285 } 286 287auto & tool_name = tools -> at (0 )-> at ("function" )-> at ("name" ); 288caps_print_stats (tool_name ,"tools[0].function.name" ); 289caps_print_stats (tools ,"tools" ); 290if (!tool_name -> stats .used ) { 291result .supports_tools = false; 292 } 293 294auto & tool_calls = messages -> at (1 )-> at ("tool_calls" );; 295caps_print_stats (tool_calls ,"messages[1].tool_calls" ); 296if (!tool_calls -> stats .used ) { 297result .supports_tool_calls = false; 298return ; 299 } 300 301auto & tool_arg = tool_calls -> at (0 )-> at ("function" )-> at ("arguments" )-> at ("arg" ); 302caps_print_stats (tool_arg ,"messages[1].tool_calls[0].function.arguments.arg" ); 303if (tool_arg -> stats .used ) { 304result .supports_object_arguments = true; 305 } 306 } 307 ); 308 309if (!result .supports_object_arguments ) { 310JJ_DEBUG ("%s\n" ,">>> Running capability check: single tool with string arguments support" ); 311 312// case: tools support: single call with string arguments 313caps_try_execute ( 314prog , 315 [& ]() { 316// messages 317return 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 }, 351nullptr ,// ctx_fn 352 [& ]() { 353// tools 354return 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 & ) { 376if (!success ) { 377result .supports_tool_calls = false; 378result .supports_tools = false; 379return ; 380 } 381 382auto & tool_name = tools -> at (0 )-> at ("function" )-> at ("name" ); 383caps_print_stats (tool_name ,"tools[0].function.name" ); 384caps_print_stats (tools ,"tools" ); 385if (!tool_name -> stats .used ) { 386result .supports_tools = false; 387 } 388 389auto & tool_calls = messages -> at (1 )-> at ("tool_calls" ); 390caps_print_stats (tool_calls ,"messages[1].tool_calls" ); 391if (!tool_calls -> stats .used ) { 392result .supports_tool_calls = false; 393return ; 394 } 395 } 396 ); 397 } 398 399JJ_DEBUG ("%s\n" ,">>> Running capability check: parallel tool support" ); 400 401// case: tools support: parallel calls 402caps_try_execute ( 403prog , 404 [& ]() { 405json args = json (R"({"arg": "value"})" ); 406if (result .supports_object_arguments ) { 407args = json {{"arg" ,"value" }}; 408 } 409 410// messages 411return 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 }, 453nullptr ,// ctx_fn 454 [& ]() { 455// tools 456return 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 & ) { 478if (!success ) { 479result .supports_parallel_tool_calls = false; 480return ; 481 } 482 483auto & tool_calls = messages -> at (1 )-> at ("tool_calls" ); 484caps_print_stats (tool_calls ,"messages[1].tool_calls" ); 485 486// check for second tool call usage 487auto & tool_call_1 = tool_calls -> at (1 )-> at ("function" ); 488caps_print_stats (tool_call_1 ,"messages[1].tool_calls[1].function" ); 489if (!tool_call_1 -> stats .used ) { 490result .supports_parallel_tool_calls = false; 491 } 492 } 493 ); 494 495JJ_DEBUG ("%s\n" ,">>> Running capability check: preserve reasoning" ); 496 497// case: preserve reasoning content in chat history 498const std::string reasoning_placeholder = "<REASONING_CONTENT_PLACEHOLDER>" ; 499caps_try_execute ( 500prog , 501 [& ]() { 502// messages 503return 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 ) { 530ctx .set_val ("enable_thinking" ,mk_val < value_bool > (true)); 531caps_apply_preserve_reasoning (ctx , true); 532 }, 533nullptr ,// 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 536if (output .find (reasoning_placeholder )!= std::string::npos ) { 537result .supports_preserve_reasoning = true; 538 } 539 } 540 ); 541 542JJ_DEBUG ("%s\n" ,">>> Running capability check: reasoning effort" ); 543 544// case: reasoning effort level 545caps_try_execute ( 546prog , 547 [& ]() { 548// messages 549return json::array ({ 550 { 551 {"role" ,"user" }, 552 {"content" ,"User message" } 553 }, 554 }); 555 }, 556 [& ](context & ctx ) { 557ctx .set_val ("enable_thinking" ,mk_val < value_bool > (true)); 558caps_apply_reasoning_effort (ctx ,"low" ); 559 }, 560nullptr ,// tools_fn 561 [& ](context & ctx ,bool ,value & ,value & ,const std::string & ) { 562value effort = ctx .get_val ("reasoning_effort" ); 563caps_print_stats (effort ,"reasoning_effort" ); 564result .supports_reasoning_effort = effort -> stats .used ; 565 } 566 ); 567 568JJ_DEBUG ("%s\n" ,result .to_string ().c_str ()); 569 570return result ; 571} 572 573}// namespace jinja