Files
Strata/serve/test_history_tools.py
Niko1221 35057071c1 Tool calls: a call written as tools.write, functions.write or default_api.write is the declared tool write (#4751)
A programmatic-tool-calling harness shows the model its tools as tools.write(...) inside code, and the model then wrote the native call with that spelling; the call came back as text and the run stopped. The prefix is read like the other unambiguous aliases (a declared name always wins, an undeclared name stays text). Also: a tool_reference with a non-string name no longer fails the request. Tests for the prefixes and for the history rule on the Anthropic and Responses shapes.
2026-10-31 12:47:49 +01:00

107 lines
5.4 KiB
Python

"""#4663: a call to a tool that this turn's `tools` no longer lists, but that the conversation already called, is a tool call (not raw text).
python -m unittest serve.test_history_tools
"""
import unittest
import serve.server as server
from serve.frontend import OutputParser
TODO = {"name": "TodoWrite", "parameters": {"type": "object", "properties": {"todos": {"type": "array"}}}}
CALL = "<tool_call>\n<function=Bash>\n<parameter=command>\nls -la\n</parameter>\n</function>\n</tool_call>"
def history(*names):
return {"messages": [{"role": "user", "content": "go"},
*[{"role": "assistant", "content": "", "tool_calls": [{"id": f"c{i}", "type": "function",
"function": {"name": n, "arguments": "{}"}}]} for i, n in enumerate(names)]]}
def run(tools, text=CALL):
p = OutputParser(thinking=False, tools=tools, stream_tools=True, recover=True, declared_only=True)
evs = p.feed(text) + p.finish("stop")
return [e.call.name for e in evs if e.kind == "tool_call"], "".join(e.text for e in evs if e.kind == "content")
class HistoryTools(unittest.TestCase):
def test_a_tool_the_conversation_called_is_a_tool_call_again(self):
names, answer = run(server.with_history_tools([TODO], history("Bash")))
self.assertEqual(names, ["Bash"])
self.assertEqual(answer, "")
def test_without_the_history_it_stays_text(self):
names, answer = run(server.with_history_tools([TODO], history("Read")))
self.assertEqual(names, [])
self.assertIn("<tool_call>", answer)
def test_declared_names_are_not_doubled_and_no_tools_stays_no_tools(self):
tools = [TODO, {"name": "Bash", "parameters": {"type": "object", "properties": {"command": {"type": "string"}}}}]
self.assertIs(server.with_history_tools(tools, history("Bash", "TodoWrite")), tools)
self.assertEqual(server.with_history_tools([], history("Bash")), [])
self.assertIsNone(server.with_history_tools(None, history("Bash")))
def test_anthropic_tool_use_blocks_count(self):
req = {"messages": [{"role": "user", "content": "go"},
{"role": "assistant", "content": [{"type": "text", "text": "ok"},
{"type": "tool_use", "id": "t1", "name": "Bash", "input": {}}]}]}
names, answer = run(server.with_history_tools([TODO], req))
self.assertEqual(names, ["Bash"])
self.assertEqual(answer, "")
def test_responses_function_call_items_count(self):
req = {"input": [{"type": "message", "role": "user", "content": "go"},
{"type": "function_call", "name": "Bash", "call_id": "c1", "arguments": "{}"}]}
names, answer = run(server.with_history_tools([TODO], req))
self.assertEqual(names, ["Bash"])
self.assertEqual(answer, "")
def test_responses_namespace_is_part_of_the_name(self):
req = {"input": [{"type": "function_call", "namespace": "mcp", "name": "Bash", "call_id": "c1", "arguments": "{}"}]}
self.assertEqual([t["name"] for t in server.with_history_tools([TODO], req)], ["TodoWrite", "mcp.Bash"])
def test_the_other_routes_without_history_stay_text(self):
names, answer = run(server.with_history_tools([TODO], {"input": [{"type": "message", "role": "user", "content": "go"}]}))
self.assertEqual(names, [])
self.assertIn("<tool_call>", answer)
def test_odd_requests_are_left_alone(self):
self.assertEqual(server.with_history_tools([TODO], None), [TODO])
self.assertEqual(server.with_history_tools([TODO], {"messages": [None, {"role": "assistant", "tool_calls": [None, {}]}]}), [TODO])
class PrefixedNames(unittest.TestCase):
"""#4751: `tools.write` for a declared `write` (a programmatic-tool-calling harness shows the model `tools.write(...)`)."""
WRITE = {"name": "write", "parameters": {"type": "object", "properties": {"file_path": {"type": "string"}, "content": {"type": "string"}}}}
TEXT = ("<tool_call>\n<function=tools.write>\n<parameter=file_path>/a/b.py</parameter>\n"
"<parameter=content>x = 1\n</parameter>\n</function>\n</tool_call>")
def calls(self, tools, text=None):
p = OutputParser(thinking=False, tools=tools, stream_tools=True, recover=True, declared_only=True)
evs = p.feed(text or self.TEXT) + p.finish("stop")
return [e.call for e in evs if e.kind == "tool_call"], "".join(e.text for e in evs if e.kind == "content")
def test_the_prefix_is_read_as_the_declared_tool(self):
calls, answer = self.calls([self.WRITE])
self.assertEqual([(c.name, c.arguments["file_path"]) for c in calls], [("write", "/a/b.py")])
self.assertEqual(answer, "")
def test_functions_prefix_too(self):
calls, _ = self.calls([self.WRITE], self.TEXT.replace("tools.write", "functions.write"))
self.assertEqual([c.name for c in calls], ["write"])
def test_an_undeclared_name_stays_text(self):
calls, answer = self.calls([self.WRITE], self.TEXT.replace("tools.write", "tools.delete"))
self.assertEqual(calls, [])
self.assertIn("<tool_call>", answer)
def test_a_declared_dotted_name_wins(self):
dotted = {"name": "tools.write", "parameters": self.WRITE["parameters"]}
calls, _ = self.calls([self.WRITE, dotted])
self.assertEqual([c.name for c in calls], ["tools.write"])
if __name__ == "__main__":
unittest.main()