feat(tools): derive Tool description and parameters from its function - #12582
feat(tools): derive Tool description and parameters from its function#12582nata2627 wants to merge 2 commits into
Conversation
…eate_tool_from_function `create_tool_from_function` did two things: derive a description and a JSON schema from a function, and build a `Tool` from them. The first half is now `_description_and_parameters_from_function`, which the old function calls. Pure move — the derivation itself is unchanged. It is separated so that `Tool` can perform the same derivation without going through a helper that constructs a `Tool` for it.
`Tool` required `description` and `parameters`, and `parameters` is a JSON schema — so constructing one by hand meant writing out the schema of a function whose signature already carried it. `create_tool_from_function` derives both, but it builds the `Tool` itself, so the derivation was out of reach for anyone who has to construct the object, which is the case when a Tool comes from a YAML or config mapping. Both fields are now optional and derived from `function` (or `async_function`) when they are not given, through the helper `create_tool_from_function` uses, so the two paths cannot describe the same function differently. `name` stays required, an explicit empty description stays empty rather than being replaced by the docstring, and passing either field behaves as before. The defaults are a module-level sentinel annotated `Any` rather than `None` with optional annotations: neither field is ever `None` after `__post_init__`, and declaring them optional put 18 mypy errors on callers that would each have had to guard a case that cannot occur. Closes deepset-ai#9006
|
@nata2627 is attempting to deploy a commit to the deepset Team on Vercel. A member of the Team first needs to authorize it. |
|
Hi @nata2627, thanks for your interest in contributing to Haystack! 🙏 This is an automated message to help us keep the review queue healthy. |
|
@sjrl do you think this is still relevant? (I'm not sure) |
|
That one is yours to answer rather than mine — whether the YAML / Pipeline Studio case @sjrl opened it from is still live is something I can't see from outside, and if it isn't, closing this costs me nothing. Two facts that might make the call cheaper, both from the last year rather than from March 2025:
So there is nothing here that decays if you leave it, and dropping it is free. |
|
See #9006 (comment) |
Related Issues
parameteranddescriptioncreation fromcreate_tool_from_functionintoTool#9006Proposed Changes:
Toolrequiresdescriptionandparametersat construction.parametersisa JSON schema, so building a tool from a function by hand means writing out
{"type": "object", "properties": {"city": {"type": "string", "description": ...}}}for a function whose signature already says all of it. Haystack derives exactly
that, correctly, in
create_tool_from_function— but that helper constructsthe
Tool, so the derivation is unreachable to anyone who has to build theobject themselves. That is the case in the issue: a tool instantiated from a
YAML/config mapping, where neither the helper nor the
@tooldecorator is inthe picture.
Both fields are now optional and derived from the tool's own
function(orasync_function) when they are not passed:The schema-and-docstring logic moved out of
create_tool_from_functioninto_description_and_parameters_from_function, which both paths now call — so aToolbuilt directly and aToolbuilt from a function cannot describe thesame function differently, including as that logic changes.
Deliberately unchanged:
namestays required. @anakin87 raised inferring it as an aside andneither maintainer asked for it; the issue is about the other two.
description=""still means an empty description, not "derive it". Onlyan omitted field is derived, which is why the default is a sentinel and not
the empty string.
included:
ComponentTool,AgentToolandPipelineToolall resolve bothvalues before calling
super().__init__, so nothing is derived for them.Two notes on the shape, both places where I went against the obvious version:
name,descriptionandparametersare the first three fields and everythingafter them already defaults, so the middle two can take defaults with no
reordering and no
field()tricks.Optional. Thesketch in the issue used
Optional[str] = None, and I tried that first: ittype-checks, but
descriptionandparametersare neverNoneonce__post_init__has run, so declaring them optional pushes an impossible caseonto every reader.
hatch run test:typesmeasured it — 18 errors across 8files,
haystack/tools/searchable_toolset.pyamong them, each one anor {}guarding a case that cannot happen, and the same would land on theintegrations repo. The sentinel default annotated
Anykeepsstranddict[str, Any]true for every consumer and needs notype: ignore, no castand no assertion — which is what
AGENTS.mdasks for. It is one line and itis commented where it sits.
How did you test it?
test/tools/test_tool.py, classTestToolDerivedFromFunction— 12 tests:both fields derived; only the missing one derived; derived values equal to
create_tool_from_function's; explicit""kept; missing docstring giving""; derivation fromasync_function;functionpreferred when both are set;Stateandinputs_from_stateparameters left out of the derived schema;tool_spec;to_dict/from_dictround trip; a parameter without a type hintraising; and no function at all still raising first.
hatch run test:unit— 6359 passed, 10 skipped, 324 deselected, 0 failedon
mainat84b90b8; 6371 passed, 10 skipped, 324 deselected, 0 failedwith this branch, which is the baseline plus the 12 new tests and nothing
else moved.
haystack/tools/tool.pyfails 11 of the 12 withTypeError: Tool.__init__() missing 2 required positional arguments. Thetwelfth is the
description=""guard, which passes either way byconstruction.
hatch run test:types—Success: no issues found in 482 source files.pre-commit run --files …— all hooks pass,release-note-backticksandcodespellincluded.Notes for the reviewer
The interesting file is
haystack/tools/tool.py.from_function.pyis a puremove in its own commit:
create_tool_from_function's body is unchanged, itjust lives in the new helper and the old function calls it.
docs-website/docs/tools/tool.mdxis in here because the page prints thedataclass signature with both fields required and tells the reader to use
@tool"so you don't need to write the schema by hand" — sentences thischange makes wrong. I touched only those, plus one worked example whose output
I ran rather than typed. Happy to drop that file if you would rather write the
docs yourselves.
One open question I could not answer from the thread: @anakin87, you wrote in
March 2025 that you would like to work on this — if it is still yours, say so
and I will close this.
Checklist
fix:,feat:,build:,chore:,ci:,docs:,style:,refactor:,perf:,test:and added!in case the PR includes breaking changes.This PR was fully generated with an AI assistant. I have reviewed the changes
and run the relevant tests.