Skip to content

Support Anthropic's native server-side tools in AnthropicChatGenerator#3555

Open
ErenAta16 wants to merge 13 commits into
deepset-ai:mainfrom
ErenAta16:add-anthropic-native-tools
Open

Support Anthropic's native server-side tools in AnthropicChatGenerator#3555
ErenAta16 wants to merge 13 commits into
deepset-ai:mainfrom
ErenAta16:add-anthropic-native-tools

Conversation

@ErenAta16

@ErenAta16 ErenAta16 commented Jul 6, 2026

Copy link
Copy Markdown

Related Issues

Proposed Changes:

Anthropic offers server-side tools like web_search, code_execution, and bash that are defined as plain dicts (e.g. {"type": "web_search_20250305", "name": "web_search"}) rather than Haystack Tool objects, since Anthropic runs them itself instead of calling back into user code. There was no way to pass these through AnthropicChatGenerator: the tools parameter only accepted Tool/Toolset instances, and passing a raw dict tripped the Tool/Toolset flattening and duplicate-name check with TypeError: Items in the tools list must be Tool or Toolset instances.

OpenAIResponsesChatGenerator already solves the equivalent problem for OpenAI's own native tools by accepting list[dict] alongside its normal ToolsType and branching on whether the first element is a dict. This mirrors that approach for Anthropic:

  • __init__, to_dict/from_dict, and _prepare_request_params (used by both run and run_async) now detect a list of dicts and pass it straight through to the Anthropic API unchanged, instead of routing it through the Haystack tool machinery.
  • AnthropicVertexChatGenerator and AnthropicFoundryChatGenerator mirror the same handling in their own __init__/to_dict/from_dict and run/run_async overrides, since they don't delegate to the base class for these.

Usage:

generator = AnthropicChatGenerator(tools=[{"type": "web_search_20250305", "name": "web_search"}])

How did you test it?

  • Added unit tests covering initialization, serialization round-trip, that run() forwards native tool dicts unchanged, and that native tools correctly skip the duplicate-name check (which only makes sense for Haystack Tool objects with a .name).
  • Verified locally: all existing + new unit tests pass, ruff check / ruff format --check are clean, and mypy reports no new errors (the two pre-existing anthropic.Anthropic/AsyncAnthropic attr-defined errors are unrelated to this change and present on main too).

Notes for the reviewer

The Vertex and Foundry generators subclass AnthropicChatGenerator but reimplement their own __init__/to_dict/from_dict instead of delegating to it, so they needed the same native-tools guard applied separately. That part is in the second and third commits.

Checklist

ErenAta16 added 2 commits July 7, 2026 02:42
Anthropic offers server-side tools like web_search, code_execution, and
bash that are defined as plain dicts (e.g. {"type": "web_search_20250305",
"name": "web_search"}) rather than Haystack Tool objects, since Anthropic
runs them itself instead of calling back into user code. There was no way
to pass these through AnthropicChatGenerator: the tools parameter only
accepted Tool/Toolset instances, and passing a raw dict tripped the
Tool/Toolset flattening and duplicate-name check.

OpenAIResponsesChatGenerator already solves the equivalent problem for
OpenAI's own native tools by accepting list[dict] alongside its normal
ToolsType and branching on whether the first element is a dict. This
mirrors that approach for Anthropic: __init__, to_dict/from_dict, and
_prepare_request_params (used by both run and run_async) now detect a
list of dicts and pass it straight through to the API unchanged, instead
of routing it through the Haystack tool machinery.
Covers initialization, serialization round-trip, and that run() passes
native tool dicts through to the Anthropic client unchanged, plus a
regression test confirming native tools skip the duplicate-name check
that only makes sense for Haystack Tool objects.
@ErenAta16 ErenAta16 requested a review from a team as a code owner July 6, 2026 23:45
@ErenAta16 ErenAta16 requested review from davidsbatista and removed request for a team July 6, 2026 23:45
@CLAassistant

CLAassistant commented Jul 6, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added integration:anthropic type:documentation Improvements or additions to documentation labels Jul 6, 2026
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Heads-up for maintainers

This PR is from a fork and touches integrations whose integration tests require API keys.
Those tests are skipped in CI because fork PRs don't have access to repo secrets for security reasons.

Affected integrations:

  • anthropic

Please run the integration tests locally (hatch run test:integration inside each folder) before approving.

ErenAta16 added 2 commits July 7, 2026 02:58
The Anthropic SDK's messages.create() accepts a narrower tools type
than the list[ToolParam] | list[dict] union we now support, so mypy
flags both call sites even though the native tool dicts are already
in the exact shape the API expects.
AnthropicVertexChatGenerator and AnthropicFoundryChatGenerator each
reimplement __init__, to_dict, and from_dict rather than delegating
to the base class, so they still assumed tools was always a Haystack
Tool/Toolset. Since both classes inherit the base class's tools
attribute type, mypy was flagging the mismatch in their own
serialization code and in Foundry's run/run_async overrides.

Mirror the same guard used in AnthropicChatGenerator: skip Haystack's
tool validation and serialization when tools is a plain list of
dicts, and widen the run/run_async signatures on Foundry to match
the base class.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Coverage report (anthropic)

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  integrations/anthropic/src/haystack_integrations/components/generators/anthropic/chat
  chat_generator.py
  foundry_chat_generator.py 315
  utils.py
  vertex_chat_generator.py 197
Project Total  

This report was generated by python-coverage-comment-action

@davidsbatista

Copy link
Copy Markdown
Contributor

Could you please update the comment and fill in our PR template to help the review? Also, please sign the CLA and then ping me again for a review

@ErenAta16 ErenAta16 closed this Jul 7, 2026
@ErenAta16

Copy link
Copy Markdown
Author

Could you please update the comment and fill in our PR template to help the review? Also, please sign the CLA and then ping me again for a review

Updated the description to follow the PR template. The CLA is signed. Ready for another look whenever you have time — thanks!

@ErenAta16 ErenAta16 reopened this Jul 7, 2026
@davidsbatista

Copy link
Copy Markdown
Contributor

There's a bug on the from_dict function, see the code below:

from haystack_integrations.components.generators.anthropic import AnthropicChatGenerator

gen = AnthropicChatGenerator(tools=[])
d = gen.to_dict()
AnthropicChatGenerator.from_dict(d)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
Cell In[5], line 1
----> 1 AnthropicChatGenerator.from_dict(d)

File ~/haystack-core-integrations/integrations/anthropic/src/haystack_integrations/components/generators/anthropic/chat/chat_generator.py:253, in AnthropicChatGenerator.from_dict(cls, data)
    249 tools = data["init_parameters"].get("tools")
    250 is_haystack_toolset = isinstance(tools, dict) and tools.get("type") == "haystack.tools.toolset.Toolset"
    251 is_haystack_tool_list = (
    252     isinstance(tools, list)
--> 253     and isinstance(tools[0], dict)
    254     and tools[0].get("type") == "haystack.tools.tool.Tool"
    255 )
    256 if tools and (is_haystack_toolset or is_haystack_tool_list):
    257     deserialize_tools_or_toolset_inplace(data["init_parameters"], key="tools")

IndexError: list index out of range
  • If a component was initialized with tools=[] to_dict() serializes it to tools=[] (haystack's serialize_tools_or_toolset([]) returns [], not None).

  • Feeding that back into from_dict() raises IndexError: list index out of range, since isinstance(tools, list) passes but tools[0] indexes an empty list.

  • Also, please refactor the checks ((is_native_tools, is_haystack_toolset, is_haystack_tool_list)) into a single function in utils.py that can then be reused by the 3 generators.

  • The bug above is duplicated, having one shared helper used by all three generators) fixes it once, and any future tweak (e.g. supporting more native-tool shapes) only needs to happen in one place.

AnthropicChatGenerator(tools=[]).to_dict() serializes the tools
parameter as [], since serialize_tools_or_toolset([]) returns [] rather
than None. Feeding that back into from_dict() raised IndexError: list
index out of range, because the Haystack-tool-list check indexed into
tools[0] before confirming the list was non-empty:

    is_haystack_tool_list = (
        isinstance(tools, list)
        and isinstance(tools[0], dict)
        ...
    )

The outer `if tools and (...)` guard at the call site did not help,
since is_haystack_tool_list was already evaluated eagerly above it.

The same pattern (a list/dict shape check used to distinguish Anthropic's
native, server-side tool dicts from Haystack Tool/Toolset objects) was
duplicated across chat_generator.py, foundry_chat_generator.py, and
vertex_chat_generator.py, in __init__, to_dict, and from_dict. Moved it
into two shared helpers in utils.py:

- _is_native_tools_list(tools): the existing, already-correct
  isinstance(tools, list) and tools and isinstance(tools[0], dict) check,
  used in __init__, to_dict, and _prepare_request_params.
- _should_deserialize_tools_or_toolset(tools): the from_dict check,
  now guarding the list-emptiness before indexing.

All three generators import and use both helpers instead of repeating
the logic inline, so a future change to how native tools are detected
only needs to happen in one place.

Added a regression test (test_serde_with_empty_tools_list) that
round-trips a generator initialized with tools=[] through to_dict/
from_dict and confirms it no longer raises. Verified it fails against
the previous implementation and passes against this change, and that
the existing native-tools serde/init/duplicate-name tests are unaffected.
@ErenAta16

Copy link
Copy Markdown
Author

Good catch, thanks. Confirmed the exact issue: to_dict() serializes an empty tools list as [] (since serialize_tools_or_toolset([]) returns [], not None), and from_dict() indexed into tools[0] to check for the Haystack-tool-list shape before checking the list was non-empty. The outer if tools and (...) guard didn't help because is_haystack_tool_list was evaluated eagerly above it, so the IndexError happened before that guard was even reached.

Pushed a fix. Also did the refactor you suggested: moved the native-tools-list check and the deserialize-or-not check into two shared helpers in utils.py (_is_native_tools_list and _should_deserialize_tools_or_toolset), used by all three generators (AnthropicChatGenerator, AnthropicFoundryChatGenerator, AnthropicVertexChatGenerator) in their __init__, to_dict, and from_dict. The bug was in fact duplicated identically across all three from_dict implementations, so this fixes it in one place instead of three.

Added test_serde_with_empty_tools_list, which round-trips AnthropicChatGenerator(tools=[]) through to_dict()/from_dict(). Ran it against the previous code first to confirm it reproduces your IndexError, then against the fix to confirm it passes, and re-ran the existing native-tools tests (test_init_with_native_tools, test_serde_with_native_tools, test_native_tools_skip_duplicate_name_check, test_from_dict) to make sure nothing else moved.

Let me know if you'd rather this be two separate commits (bugfix, then refactor) instead of one — happy to split it if that's easier to review.

ErenAta16 added 2 commits July 9, 2026 21:36
The docstring's summary wrapped onto a second physical line before the
blank line separating it from the description, which fails this
project's D205 rule (summary and description must be separated by
exactly one blank line). Put the summary on a single line instead.

Confirmed with the project's own ruff config (pyproject.toml select
list, line-length 120): ruff check --select D205 and ruff format
--check both pass on the changed file after this fix, matching what
CI's fmt-check step runs.
Moving the isinstance(tools, list) and tools and isinstance(tools[0],
dict) check out of chat_generator.py and into a plain bool-returning
helper lost mypy's type narrowing at the call site in
_prepare_request_params: `anthropic_tools = tools` inside the
`if _is_native_tools_list(tools):` branch no longer narrows `tools`
from its declared ToolsType | list[dict] | None to list[dict], so
mypy flagged an incompatible assignment.

Annotated the helper's return type as TypeGuard[list[dict[str, Any]]]
instead of bool, which restores narrowing for mypy while keeping the
single shared implementation. Confirmed with mypy -p haystack_
integrations.components.generators.anthropic (as run by this
project's test:types script): no errors in 7 source files. Also
re-ran ruff check/format and the full non-integration test suite to
confirm nothing else regressed.
@ErenAta16

Copy link
Copy Markdown
Author

CI is green now (the two intermediate failures were a lint docstring formatting issue and a mypy type-narrowing regression from the refactor, both fixed in the follow-up commits).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration:anthropic type:documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for Anthropic native web_search tool

3 participants