faizath's picture
feat: add the trained sahabat-ai 8b lora adapter
0604598 verified
Raw
History Blame
3.54 kB
{#- 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 `<tool_response>` 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": <nama fungsi>, "parameters": <objek argumen>}, 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 `<tool_response>`, 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<tool_response>\n' + message['content'] | trim + '\n</tool_response><|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 %}