{#- Tool-capable Llama-3 chat template for the Fairleap corpus. The stock Sahabat-AI template renders every message as `content | trim` and ignores both `tool_calls` and the `tools` argument. On this corpus that is silent data loss, not an error: an assistant tool-call turn carries `content: ""`, so it renders as an *empty* assistant reply and the call disappears. ~10% of the corpus is shaped that way. Two things this adds: 1. assistant `tool_calls` render as the one-line JSON the model is meant to emit, and `role: tool` results come back as a `user` turn wrapping `` so response-only masking still works (see the note on that branch); 2. the `tools` schema list is folded into the system turn, so "tool offered" and "tool not offered" are distinguishable in context. Without that the model cannot learn when *not* to call. -#} {{- bos_token }} {%- set has_system = messages and messages[0]['role'] == 'system' %} {%- set tool_header = 'Kamu punya akses ke fungsi berikut. Untuk memanggil fungsi, balas HANYA dengan satu baris JSON berbentuk {"name": , "parameters": }, tanpa teks lain. Panggil fungsi hanya jika pertanyaan driver memang membutuhkannya.\n\nFungsi yang tersedia:' %} {%- if tools and not has_system %} {{- '<|start_header_id|>system<|end_header_id|>\n\n' + tool_header }} {%- for tool in tools %} {{- '\n' }}{{- tool['function'] | tojson }} {%- endfor %} {{- '<|eot_id|>' }} {%- endif %} {%- for message in messages %} {%- if message['role'] == 'system' %} {{- '<|start_header_id|>system<|end_header_id|>\n\n' + message['content'] | trim }} {%- if tools and loop.first %} {{- '\n\n' + tool_header }} {%- for tool in tools %} {{- '\n' }}{{- tool['function'] | tojson }} {%- endfor %} {%- endif %} {{- '<|eot_id|>' }} {%- elif message['role'] == 'assistant' and message.get('tool_calls') %} {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} {%- for tool_call in message['tool_calls'] %} {#- Emit each piece separately: `tojson` returns Markup, and concatenating a plain string with it HTML-escapes the quotes. #} {{- '{"name": "' }}{{- tool_call['function']['name'] }}{{- '", "parameters": ' }} {{- tool_call['function']['arguments'] | tojson }}{{- '}' }} {%- endfor %} {{- '<|eot_id|>' }} {%- elif message['role'] == 'tool' %} {#- Deliberately a `user` turn wrapping ``, not Llama-3.1's `ipython` header. `train_on_responses_only` masks from the response delimiter to the next *instruction* delimiter, and an `ipython` header matches neither -- the tool result would land inside the loss and teach the model to invent forecasts. This is also byte-for-byte the shape Qwen's own template uses, so both Fairleap adapters take tool results in the same form. #} {{- '<|start_header_id|>user<|end_header_id|>\n\n\n' + message['content'] | trim + '\n<|eot_id|>' }} {%- else %} {{- '<|start_header_id|>' + message['role'] + '<|end_header_id|>\n\n' + message['content'] | trim + '<|eot_id|>' }} {%- endif %} {%- endfor %} {%- if add_generation_prompt %} {{- '<|start_header_id|>assistant<|end_header_id|>\n\n' }} {%- endif %}