From 759c2551acd0adb996af9a80aaeb067a7b4b5b8f Mon Sep 17 00:00:00 2001 From: Andrew Sanchez Date: Mon, 3 Nov 2025 04:54:01 -0500 Subject: [PATCH 1/4] Propagate schema through the chain. --- llm/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/llm/models.py b/llm/models.py index 5e7676eb..dca0c4da 100644 --- a/llm/models.py +++ b/llm/models.py @@ -1625,6 +1625,7 @@ def responses(self) -> Iterator[Response]: tool_results=tool_results, options=self.prompt.options, attachments=attachments, + schema=current_response.prompt.schema, ), self.model, stream=self.stream, @@ -1681,6 +1682,7 @@ async def responses(self) -> AsyncIterator[AsyncResponse]: tool_results=tool_results, options=self.prompt.options, attachments=attachments, + schema=current_response.prompt.schema, ) current_response = AsyncResponse( prompt, From a26662d78c89bd46fccc82dacb116b435d2e14b2 Mon Sep 17 00:00:00 2001 From: Andrew Sanchez Date: Wed, 31 Dec 2025 08:07:14 -0500 Subject: [PATCH 2/4] Add tests. --- tests/test_tools.py | 47 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_tools.py b/tests/test_tools.py index c61154f2..36623fb4 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -520,6 +520,53 @@ def test_tool_errors(async_): ) in log_text_result.output +def test_schema_propagates_through_tool_chain(): + """Test that schema is propagated through tool chains.""" + model = llm.get_model("echo") + model.supports_schema = True + + def get_dog() -> str: + return "Cleo is 10 years old" + + dog_schema = {"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}} + + chain_response = model.chain( + json.dumps({"tool_calls": [{"name": "get_dog"}]}), + tools=[get_dog], + schema=dog_schema, + ) + _ = chain_response.text() + + assert len(chain_response._responses) == 2 + first, second = chain_response._responses + assert first.prompt.schema == dog_schema + assert second.prompt.schema == dog_schema + + +@pytest.mark.asyncio +async def test_schema_propagates_through_tool_chain_async(): + """Test schema propagation through tool chains for async models.""" + model = llm.get_async_model("echo") + model.supports_schema = True + + async def get_dog() -> str: + return "Cleo is 10 years old" + + dog_schema = {"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}} + + chain_response = model.chain( + json.dumps({"tool_calls": [{"name": "get_dog"}]}), + tools=[get_dog], + schema=dog_schema, + ) + _ = await chain_response.text() + + assert len(chain_response._responses) == 2 + first, second = chain_response._responses + assert first.prompt.schema == dog_schema + assert second.prompt.schema == dog_schema + + def test_chain_sync_cancel_only_first_of_two(): model = llm.get_model("echo") From f1ce873a9fbfd27db3f3d084ce635d2b60e59c3b Mon Sep 17 00:00:00 2001 From: Andrew Sanchez Date: Wed, 31 Dec 2025 08:08:04 -0500 Subject: [PATCH 3/4] Update docs. --- README.md | 1 + docs/tools.md | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/README.md b/README.md index 92cde72d..08e925d8 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ See also [the llm tag](https://simonwillison.net/tags/llm/) on my blog. * [Trying out tools](https://llm.datasette.io/en/stable/tools.html#trying-out-tools) * [LLM’s implementation of tools](https://llm.datasette.io/en/stable/tools.html#llm-s-implementation-of-tools) * [Default tools](https://llm.datasette.io/en/stable/tools.html#default-tools) + * [Combining tools with schemas](https://llm.datasette.io/en/stable/tools.html#combining-tools-with-schemas) * [Tips for implementing tools](https://llm.datasette.io/en/stable/tools.html#tips-for-implementing-tools) * [Schemas](https://llm.datasette.io/en/stable/schemas.html) * [Schemas tutorial](https://llm.datasette.io/en/stable/schemas.html#schemas-tutorial) diff --git a/docs/tools.md b/docs/tools.md index a6760074..69c733a7 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -89,6 +89,25 @@ Try them like this: llm -T llm_version -T llm_time 'Give me the current time and LLM version' --td ``` +(tools-with-schemas)= + +## Combining tools with schemas + +You can use tools and {ref}`schemas ` together. + +```bash +llm --tool llm_time --schema "date: current date" "What is the time?" --td +``` +Example output: +``` +Tool call: llm_time({}) + {"utc_time": "2025-02-28 14:30:00 UTC", ...} + +{"date": "2025-02-28"} +``` + +The model first calls the `llm_time` tool to get the current time, then uses that information to produce a response that matches the schema. + (tools-tips)= ## Tips for implementing tools From e64f8fea2071e55fa538a9167d3045f183a4d696 Mon Sep 17 00:00:00 2001 From: Andrew Sanchez Date: Wed, 31 Dec 2025 11:09:52 -0500 Subject: [PATCH 4/4] Blacken. --- tests/test_tools.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 36623fb4..64f5ad33 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -528,7 +528,10 @@ def test_schema_propagates_through_tool_chain(): def get_dog() -> str: return "Cleo is 10 years old" - dog_schema = {"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}} + dog_schema = { + "type": "object", + "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, + } chain_response = model.chain( json.dumps({"tool_calls": [{"name": "get_dog"}]}), @@ -552,7 +555,10 @@ async def test_schema_propagates_through_tool_chain_async(): async def get_dog() -> str: return "Cleo is 10 years old" - dog_schema = {"type": "object", "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}} + dog_schema = { + "type": "object", + "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, + } chain_response = model.chain( json.dumps({"tool_calls": [{"name": "get_dog"}]}),