", q="from=x")',
+ [{"html": "
", "q": "from=x"}],
+ ),
+ ("wx.get(flags=[true, false]*2)", [{"flags": [True, False, True, False]}]),
+ ("wx.get(q='a')\r\nwx.get(q='b')", [{"q": "a"}, {"q": "b"}]),
+ ("Get Player's Media(id=1)\nGet Player's Media(id=2)", [{"id": 1}, {"id": 2}]),
+ ('wx.get(q=")\tget(")', [{"q": ")\tget("}]),
+ ("wx.get(q=list(range(3)), r=[[0] * 2] * 2)", [{"q": [0, 1, 2], "r": [[0, 0], [0, 0]]}]),
+ ("wx.get(q=[1] * (2 * 2))", [{"q": [1, 1, 1, 1]}]),
+ ],
+)
+def test_dolci_tool_use_repairs_call_lines(function_calls, expected):
+ assert _arguments(dolci_tool_use(_dolci_row(function_calls))) == expected
+
+
+@pytest.mark.parametrize(
+ "function_calls",
+ [
+ "wx.get(q=X)",
+ "wx.get(q=[random numbers])",
+ "wx.get(q=other(q=1))",
+ "min([2, 4, 6])",
+ "wx.get(q=(1, 2))",
+ "wx.get(q={1: 'a'})",
+ "wx.get(q={[]: 1})",
+ "wx.get(q=[0]*1000000000)",
+ "wx.get(q=list(range(1000000000)))",
+ "wx.get(q=[[0] * 10000] * 10000)",
+ "wx.get(q=1 << 100)",
+ "wx.get(q=[x for x in 'ab' for y in 'cd'])",
+ "wx.get(q=2**10)",
+ 'say"hi(q=1)',
+ "wx.get",
+ ],
+)
+def test_dolci_tool_use_drops_row_with_unconvertible_call(function_calls):
+ assert dolci_tool_use(_dolci_row(function_calls)) == {"messages": [], "tools": None}
+
+
+def test_dolci_tool_use_value_cannot_change_how_later_rows_evaluate():
+ assert not dolci_tool_use(_dolci_row("wx.get(q=[0 for __builtins__['X'] in ['Oslo']])"))[
+ "messages"
+ ]
+ assert not dolci_tool_use(_dolci_row("wx.get(q=X)"))["messages"]
+
+
+def test_olmo3_instruct_sft_rejects_json_string_arguments_with_a_clear_error():
+ chat_template_utils = pytest.importorskip("transformers.utils.chat_template_utils")
+ compiled = chat_template_utils._compile_jinja_template(get_chat_template("olmo3-instruct-sft"))
+ out = dolci_tool_use(_dolci_row('wx.get(q="Oslo")'))
+ with pytest.raises(Exception, match="use transform: null"):
+ chat_template_utils._render_with_assistant_indices(
+ compiled, out["messages"], json.loads(out["tools"]), None, False
+ )
+
+
+def test_dolci_tool_use_restores_names_that_lost_an_escaped_letter():
+ # Dolci decoded "\reverse" as "\r" + "everse"; "\next" became a plain line break.
+ row = _dolci_row("wx.get(q='a')\reverse(s='x')\x0clatten(x=1)\next(n=2)")
+ row["messages"][0]["functions"] = json.dumps(
+ [
+ {"type": "function", "function": {"name": n}}
+ for n in ("wx.get", "reverse", "flatten", "next")
+ ]
+ )
+ calls = dolci_tool_use(row)["messages"][1]["tool_calls"]
+ assert [c["function"]["name"] for c in calls] == ["wx.get", "reverse", "flatten", "next"]
+
+
+def test_dolci_tool_use_null_call_keeps_refusal_and_drops_empty_turn():
+ refusal = dolci_tool_use(_dolci_row("null", content="I can't do that."))
+ assert refusal["messages"][1] == {"role": "assistant", "content": "I can't do that."}
+
+ empty = dolci_tool_use(_dolci_row("null"))
+ assert [m["role"] for m in empty["messages"]] == ["user", "tool", "assistant"]
+
+
+def test_dolci_tool_use_keeps_task_instructions_in_system_prompt():
+ system = (
+ _DOLCI_TOOL_PROMPT
+ + " You do research.\n\n- Use tools. Format calls like: tool_name(query=...) (below)."
+ )
+ out = dolci_tool_use(_dolci_row("wx.get(q='a')", system=system))
+
+ assert out["messages"][0] == {
+ "role": "system",
+ "content": "You do research.\n\n- Use tools. (below).",
+ }
+
+
+def test_dolci_tool_use_passes_rows_without_tools():
+ row = {
+ "messages": [{"role": "user", "content": "Hi", "functions": None, "function_calls": None}]
+ }
+
+ assert dolci_tool_use(row) == {"messages": [{"role": "user", "content": "Hi"}], "tools": None}
+
+
+def test_dolci_tool_use_survives_features_cast_and_renders_qwen3_tool_call(monkeypatch):
+ """The transform output must keep `tool_calls` through the loader's
+ MESSAGES_FEATURES cast and render as a Qwen3 / hermes ``."""
+ plain = {
+ "messages": [{"role": "user", "content": "Hi", "functions": None, "function_calls": None}]
+ }
+ rows = [_dolci_row('wx.get(q="Oslo")'), plain, _dolci_row("wx.get(q=X)")]
+ monkeypatch.setattr(loader, "load_dataset", lambda path, **kwargs: Dataset.from_list(rows))
+
+ ds = loader.load_and_mix_datasets(
+ DataConfig(
+ num_proc=1,
+ datasets=[DatasetEntry(name="dolci", path="dolci", transform="dolci_tool_use")],
+ ),
+ columns_to_keep=["messages", "tools"],
+ features=MESSAGES_FEATURES,
+ )
+ tool_row = next(r for r in ds if r["tools"])
+ plain_row = next(r for r in ds if not r["tools"] and r["messages"])
+
+ assert sum(not r["messages"] for r in ds) == 1 # the X placeholder row
+
+ assert plain_row["messages"] == [{"role": "user", "content": "Hi", "tool_calls": None}]
+ chat_template_utils = pytest.importorskip("transformers.utils.chat_template_utils")
+ compiled = chat_template_utils._compile_jinja_template(get_chat_template("qwen3"))
+ rendered, indices = chat_template_utils._render_with_assistant_indices(
+ compiled, tool_row["messages"], json.loads(tool_row["tools"]), None, False
+ )
+ spans = [rendered[start:end] for start, end in indices]
+
+ assert '\n{"type": "function", "function": {"name": "wx.get"' in rendered
+ assert '\n{"temp": 5}\n' in rendered
+ assert (
+ spans[0]
+ == '\n{"name": "wx.get", "arguments": {"q": "Oslo"}}\n<|im_end|>'
+ )