Skip to content

feat(tools): derive Tool description and parameters from its function - #12582

Closed
nata2627 wants to merge 2 commits into
deepset-ai:mainfrom
nata2627:feat/tool-infer-description-and-parameters
Closed

feat(tools): derive Tool description and parameters from its function#12582
nata2627 wants to merge 2 commits into
deepset-ai:mainfrom
nata2627:feat/tool-infer-description-and-parameters

Conversation

@nata2627

@nata2627 nata2627 commented Sep 3, 2026

Copy link
Copy Markdown

Related Issues

Proposed Changes:

Tool requires description and parameters at construction. parameters is
a 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 constructs
the Tool, so the derivation is unreachable to anyone who has to build the
object themselves. That is the case in the issue: a tool instantiated from a
YAML/config mapping, where neither the helper nor the @tool decorator is in
the picture.

Both fields are now optional and derived from the tool's own function (or
async_function) when they are not passed:

def get_weather(
    city: Annotated[str, "the city for which to get the weather"] = "Munich",
    unit: Annotated[Literal["Celsius", "Fahrenheit"], "the unit"] = "Celsius",
) -> str:
    """A simple function to get the current weather for a location."""
    ...

Tool(name="get_weather", function=get_weather)
# description: 'A simple function to get the current weather for a location.'
# parameters:  the same schema create_tool_from_function(get_weather) produces

The schema-and-docstring logic moved out of create_tool_from_function into
_description_and_parameters_from_function, which both paths now call — so a
Tool built directly and a Tool built from a function cannot describe the
same function differently, including as that logic changes.

Deliberately unchanged:

  • name stays required. @anakin87 raised inferring it as an aside and
    neither maintainer asked for it; the issue is about the other two.
  • description="" still means an empty description, not "derive it". Only
    an omitted field is derived, which is why the default is a sentinel and not
    the empty string.
  • Passing either field explicitly behaves exactly as before, subclasses
    included: ComponentTool, AgentTool and PipelineTool all resolve both
    values before calling super().__init__, so nothing is derived for them.

Two notes on the shape, both places where I went against the obvious version:

  • The ordering problem raised in the issue does not arise. name,
    description and parameters are the first three fields and everything
    after them already defaults, so the middle two can take defaults with no
    reordering and no field() tricks.
  • The fields keep their real types instead of becoming Optional. The
    sketch in the issue used Optional[str] = None, and I tried that first: it
    type-checks, but description and parameters are never None once
    __post_init__ has run, so declaring them optional pushes an impossible case
    onto every reader. hatch run test:types measured it — 18 errors across 8
    files
    , haystack/tools/searchable_toolset.py among them, each one an or {} guarding a case that cannot happen, and the same would land on the
    integrations repo. The sentinel default annotated Any keeps str and
    dict[str, Any] true for every consumer and needs no type: ignore, no cast
    and no assertion — which is what AGENTS.md asks for. It is one line and it
    is commented where it sits.

How did you test it?

test/tools/test_tool.py, class TestToolDerivedFromFunction — 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 from async_function; function preferred when both are set;
State and inputs_from_state parameters left out of the derived schema;
tool_spec; to_dict/from_dict round trip; a parameter without a type hint
raising; and no function at all still raising first.

  • hatch run test:unit6359 passed, 10 skipped, 324 deselected, 0 failed
    on main at 84b90b8; 6371 passed, 10 skipped, 324 deselected, 0 failed
    with this branch, which is the baseline plus the 12 new tests and nothing
    else moved.
  • Reverting only haystack/tools/tool.py fails 11 of the 12 with
    TypeError: Tool.__init__() missing 2 required positional arguments. The
    twelfth is the description="" guard, which passes either way by
    construction.
  • hatch run test:typesSuccess: no issues found in 482 source files.
  • pre-commit run --files … — all hooks pass, release-note-backticks and
    codespell included.

Notes for the reviewer

The interesting file is haystack/tools/tool.py. from_function.py is a pure
move in its own commit: create_tool_from_function's body is unchanged, it
just lives in the new helper and the old function calls it.

docs-website/docs/tools/tool.mdx is in here because the page prints the
dataclass signature with both fields required and tells the reader to use
@tool "so you don't need to write the schema by hand" — sentences this
change 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

  • I have read the contributors guidelines and the code of conduct.
  • I have updated the related issue with new insights and changes.
  • I have added unit tests and updated the docstrings.
  • I've used one of the conventional commit types for my PR title: fix:, feat:, build:, chore:, ci:, docs:, style:, refactor:, perf:, test: and added ! in case the PR includes breaking changes.
  • I have documented my code.
  • I have added a release note file, following the contributors guidelines.
  • I have run pre-commit hooks and fixed any issue.

This PR was fully generated with an AI assistant. I have reviewed the changes
and run the relevant tests.

…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
nata2627 requested a review from a team as a code owner September 3, 2026 09:20
@nata2627
nata2627 requested review from davidsbatista and removed request for a team September 3, 2026 09:20
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

@nata2627 is attempting to deploy a commit to the deepset Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Hi @nata2627, thanks for your interest in contributing to Haystack! 🙏

⚠️ You currently have 3 open pull requests in this repository (#12548, #12539 and this one). Our review capacity is limited, so please hold off opening more PRs until we've had a chance to review your first 2 open PRs. This helps us give each contribution the attention it deserves. Thank you!

This is an automated message to help us keep the review queue healthy.

@anakin87

anakin87 commented Sep 3, 2026

Copy link
Copy Markdown
Member

@sjrl do you think this is still relevant? (I'm not sure)

@nata2627

nata2627 commented Sep 3, 2026

Copy link
Copy Markdown
Author

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:

  • The issue kept attracting people. @winklemad asked in July whether a PR would be welcome, and @gaurav0107 opened one (feat: derive Tool description and parameters from function #12023) — a full implementation, closed in August only because the CLA went unsigned, without a review either way.
  • It is additive. No existing construction changes, ComponentTool / AgentTool / PipelineTool resolve both fields before super().__init__ so nothing is derived for them, serialization round-trips unchanged, and no file outside haystack/tools/ needed touching. The full suite is 6359 → 6371 passed, the extra 12 being the new tests.

So there is nothing here that decays if you leave it, and dropping it is free.

@anakin87

anakin87 commented Sep 3, 2026

Copy link
Copy Markdown
Member

See #9006 (comment)

@anakin87 anakin87 closed this Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add parameter and description creation from create_tool_from_function into Tool

2 participants