mirror of
https://github.com/hwchase17/langchain.git
synced 2025-09-12 21:11:43 +00:00
anthropic: support for code execution, MCP connector, files API features (#31340)
Support for the new [batch of beta features](https://www.anthropic.com/news/agent-capabilities-api) released yesterday: - [Code execution](https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool) - [MCP connector](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector) - [Files API](https://docs.anthropic.com/en/docs/build-with-claude/files) Also verified support for [prompt cache TTL](https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration-beta).
This commit is contained in:
@@ -129,6 +129,7 @@ def _format_for_tracing(messages: list[BaseMessage]) -> list[BaseMessage]:
|
||||
isinstance(block, dict)
|
||||
and block.get("type") == "image"
|
||||
and is_data_content_block(block)
|
||||
and block.get("source_type") != "id"
|
||||
):
|
||||
if message_to_trace is message:
|
||||
message_to_trace = message.model_copy()
|
||||
|
@@ -193,6 +193,7 @@ def test_configurable_with_default() -> None:
|
||||
"name": None,
|
||||
"disable_streaming": False,
|
||||
"model": "claude-3-sonnet-20240229",
|
||||
"mcp_servers": None,
|
||||
"max_tokens": 1024,
|
||||
"temperature": None,
|
||||
"thinking": None,
|
||||
@@ -203,6 +204,7 @@ def test_configurable_with_default() -> None:
|
||||
"stop_sequences": None,
|
||||
"anthropic_api_url": "https://api.anthropic.com",
|
||||
"anthropic_api_key": SecretStr("bar"),
|
||||
"betas": None,
|
||||
"default_headers": None,
|
||||
"model_kwargs": {},
|
||||
"streaming": False,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
import warnings
|
||||
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
|
||||
@@ -100,6 +101,7 @@ def _is_builtin_tool(tool: Any) -> bool:
|
||||
"computer_",
|
||||
"bash_",
|
||||
"web_search_",
|
||||
"code_execution_",
|
||||
]
|
||||
return any(tool_type.startswith(prefix) for prefix in _builtin_tool_prefixes)
|
||||
|
||||
@@ -219,6 +221,14 @@ def _format_data_content_block(block: dict) -> dict:
|
||||
"data": block["data"],
|
||||
},
|
||||
}
|
||||
elif block["source_type"] == "id":
|
||||
formatted_block = {
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "file",
|
||||
"file_id": block["id"],
|
||||
},
|
||||
}
|
||||
else:
|
||||
raise ValueError(
|
||||
"Anthropic only supports 'url' and 'base64' source_type for image "
|
||||
@@ -252,6 +262,14 @@ def _format_data_content_block(block: dict) -> dict:
|
||||
"data": block["text"],
|
||||
},
|
||||
}
|
||||
elif block["source_type"] == "id":
|
||||
formatted_block = {
|
||||
"type": "document",
|
||||
"source": {
|
||||
"type": "file",
|
||||
"file_id": block["id"],
|
||||
},
|
||||
}
|
||||
|
||||
else:
|
||||
raise ValueError(f"Block of type {block['type']} is not supported.")
|
||||
@@ -340,6 +358,29 @@ def _format_messages(
|
||||
else:
|
||||
block.pop("text", None)
|
||||
content.append(block)
|
||||
elif block["type"] in ("server_tool_use", "mcp_tool_use"):
|
||||
formatted_block = {
|
||||
k: v
|
||||
for k, v in block.items()
|
||||
if k
|
||||
in (
|
||||
"type",
|
||||
"id",
|
||||
"input",
|
||||
"name",
|
||||
"server_name", # for mcp_tool_use
|
||||
"cache_control",
|
||||
)
|
||||
}
|
||||
# Attempt to parse streamed output
|
||||
if block.get("input") == {} and "partial_json" in block:
|
||||
try:
|
||||
input_ = json.loads(block["partial_json"])
|
||||
if input_:
|
||||
formatted_block["input"] = input_
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
content.append(formatted_block)
|
||||
elif block["type"] == "text":
|
||||
text = block.get("text", "")
|
||||
# Only add non-empty strings for now as empty ones are not
|
||||
@@ -375,6 +416,25 @@ def _format_messages(
|
||||
[HumanMessage(block["content"])]
|
||||
)[1][0]["content"]
|
||||
content.append({**block, **{"content": tool_content}})
|
||||
elif block["type"] in (
|
||||
"code_execution_tool_result",
|
||||
"mcp_tool_result",
|
||||
"web_search_tool_result",
|
||||
):
|
||||
content.append(
|
||||
{
|
||||
k: v
|
||||
for k, v in block.items()
|
||||
if k
|
||||
in (
|
||||
"type",
|
||||
"content",
|
||||
"tool_use_id",
|
||||
"is_error", # for mcp_tool_result
|
||||
"cache_control",
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
content.append(block)
|
||||
else:
|
||||
@@ -472,20 +532,21 @@ class ChatAnthropic(BaseChatModel):
|
||||
**NOTE**: Any param which is not explicitly supported will be passed directly to the
|
||||
``anthropic.Anthropic.messages.create(...)`` API every time to the model is
|
||||
invoked. For example:
|
||||
.. code-block:: python
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
import anthropic
|
||||
.. code-block:: python
|
||||
|
||||
ChatAnthropic(..., extra_headers={}).invoke(...)
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
import anthropic
|
||||
|
||||
# results in underlying API call of:
|
||||
ChatAnthropic(..., extra_headers={}).invoke(...)
|
||||
|
||||
anthropic.Anthropic(..).messages.create(..., extra_headers={})
|
||||
# results in underlying API call of:
|
||||
|
||||
# which is also equivalent to:
|
||||
anthropic.Anthropic(..).messages.create(..., extra_headers={})
|
||||
|
||||
ChatAnthropic(...).invoke(..., extra_headers={})
|
||||
# which is also equivalent to:
|
||||
|
||||
ChatAnthropic(...).invoke(..., extra_headers={})
|
||||
|
||||
Invoke:
|
||||
.. code-block:: python
|
||||
@@ -645,6 +706,35 @@ class ChatAnthropic(BaseChatModel):
|
||||
|
||||
"After examining both images carefully, I can see that they are actually identical."
|
||||
|
||||
.. dropdown:: Files API
|
||||
|
||||
You can also pass in files that are managed through Anthropic's
|
||||
`Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`_:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-20250514",
|
||||
betas=["files-api-2025-04-14"],
|
||||
)
|
||||
input_message = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Describe this document.",
|
||||
},
|
||||
{
|
||||
"type": "image",
|
||||
"source_type": "id",
|
||||
"id": "file_abc123...",
|
||||
},
|
||||
],
|
||||
}
|
||||
llm.invoke([input_message])
|
||||
|
||||
PDF input:
|
||||
See `multimodal guides <https://python.langchain.com/docs/how_to/multimodal_inputs/>`_
|
||||
for more detail.
|
||||
@@ -681,6 +771,35 @@ class ChatAnthropic(BaseChatModel):
|
||||
|
||||
"This appears to be a simple document..."
|
||||
|
||||
.. dropdown:: Files API
|
||||
|
||||
You can also pass in files that are managed through Anthropic's
|
||||
`Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`_:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-20250514",
|
||||
betas=["files-api-2025-04-14"],
|
||||
)
|
||||
input_message = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Describe this document.",
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"source_type": "id",
|
||||
"id": "file_abc123...",
|
||||
},
|
||||
],
|
||||
}
|
||||
llm.invoke([input_message])
|
||||
|
||||
Extended thinking:
|
||||
Claude 3.7 Sonnet supports an
|
||||
`extended thinking <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking>`_
|
||||
@@ -797,7 +916,7 @@ class ChatAnthropic(BaseChatModel):
|
||||
or by setting ``stream_usage=False`` when initializing ChatAnthropic.
|
||||
|
||||
Prompt caching:
|
||||
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`_
|
||||
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/#built-in-tools>`_
|
||||
for more detail.
|
||||
|
||||
.. code-block:: python
|
||||
@@ -834,6 +953,24 @@ class ChatAnthropic(BaseChatModel):
|
||||
|
||||
{'cache_read': 0, 'cache_creation': 1458}
|
||||
|
||||
.. dropdown:: Extended caching
|
||||
|
||||
The cache lifetime is 5 minutes by default. If this is too short, you can
|
||||
apply one hour caching by enabling the ``"extended-cache-ttl-2025-04-11"``
|
||||
beta header:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-3-7-sonnet-20250219",
|
||||
betas=["extended-cache-ttl-2025-04-11"],
|
||||
)
|
||||
|
||||
and specifying ``"cache_control": {"type": "ephemeral", "ttl": "1h"}``.
|
||||
|
||||
See `Claude documentation <https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration-beta>`_
|
||||
for detail.
|
||||
|
||||
Token-efficient tool use (beta):
|
||||
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`_
|
||||
for more detail.
|
||||
@@ -875,7 +1012,7 @@ class ChatAnthropic(BaseChatModel):
|
||||
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`_
|
||||
for more detail.
|
||||
|
||||
Web search:
|
||||
.. dropdown:: Web search
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -890,7 +1027,53 @@ class ChatAnthropic(BaseChatModel):
|
||||
"How do I update a web app to TypeScript 5.5?"
|
||||
)
|
||||
|
||||
Text editor:
|
||||
.. dropdown:: Code execution
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-20250514",
|
||||
betas=["code-execution-2025-05-22"],
|
||||
)
|
||||
|
||||
tool = {"type": "code_execution_20250522", "name": "code_execution"}
|
||||
llm_with_tools = llm.bind_tools([tool])
|
||||
|
||||
response = llm_with_tools.invoke(
|
||||
"Calculate the mean and standard deviation of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
|
||||
)
|
||||
|
||||
.. dropdown:: Remote MCP
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
mcp_servers = [
|
||||
{
|
||||
"type": "url",
|
||||
"url": "https://mcp.deepwiki.com/mcp",
|
||||
"name": "deepwiki",
|
||||
"tool_configuration": { # optional configuration
|
||||
"enabled": True,
|
||||
"allowed_tools": ["ask_question"],
|
||||
},
|
||||
"authorization_token": "PLACEHOLDER", # optional authorization
|
||||
}
|
||||
]
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-20250514",
|
||||
betas=["mcp-client-2025-04-04"],
|
||||
mcp_servers=mcp_servers,
|
||||
)
|
||||
|
||||
response = llm.invoke(
|
||||
"What transport protocols does the 2025-03-26 version of the MCP "
|
||||
"spec (modelcontextprotocol/modelcontextprotocol) support?"
|
||||
)
|
||||
|
||||
.. dropdown:: Text editor
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -986,6 +1169,13 @@ class ChatAnthropic(BaseChatModel):
|
||||
default_headers: Optional[Mapping[str, str]] = None
|
||||
"""Headers to pass to the Anthropic clients, will be used for every API call."""
|
||||
|
||||
betas: Optional[list[str]] = None
|
||||
"""List of beta features to enable. If specified, invocations will be routed
|
||||
through client.beta.messages.create.
|
||||
|
||||
Example: ``betas=["mcp-client-2025-04-04"]``
|
||||
"""
|
||||
|
||||
model_kwargs: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
streaming: bool = False
|
||||
@@ -1000,6 +1190,13 @@ class ChatAnthropic(BaseChatModel):
|
||||
"""Parameters for Claude reasoning,
|
||||
e.g., ``{"type": "enabled", "budget_tokens": 10_000}``"""
|
||||
|
||||
mcp_servers: Optional[list[dict[str, Any]]] = None
|
||||
"""List of MCP servers to use for the request.
|
||||
|
||||
Example: ``mcp_servers=[{"type": "url", "url": "https://mcp.example.com/mcp",
|
||||
"name": "example-mcp"}]``
|
||||
"""
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
"""Return type of chat model."""
|
||||
@@ -1007,7 +1204,10 @@ class ChatAnthropic(BaseChatModel):
|
||||
|
||||
@property
|
||||
def lc_secrets(self) -> dict[str, str]:
|
||||
return {"anthropic_api_key": "ANTHROPIC_API_KEY"}
|
||||
return {
|
||||
"anthropic_api_key": "ANTHROPIC_API_KEY",
|
||||
"mcp_servers": "ANTHROPIC_MCP_SERVERS",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def is_lc_serializable(cls) -> bool:
|
||||
@@ -1099,6 +1299,8 @@ class ChatAnthropic(BaseChatModel):
|
||||
"top_k": self.top_k,
|
||||
"top_p": self.top_p,
|
||||
"stop_sequences": stop or self.stop_sequences,
|
||||
"betas": self.betas,
|
||||
"mcp_servers": self.mcp_servers,
|
||||
"system": system,
|
||||
**self.model_kwargs,
|
||||
**kwargs,
|
||||
@@ -1107,6 +1309,18 @@ class ChatAnthropic(BaseChatModel):
|
||||
payload["thinking"] = self.thinking
|
||||
return {k: v for k, v in payload.items() if v is not None}
|
||||
|
||||
def _create(self, payload: dict) -> Any:
|
||||
if "betas" in payload:
|
||||
return self._client.beta.messages.create(**payload)
|
||||
else:
|
||||
return self._client.messages.create(**payload)
|
||||
|
||||
async def _acreate(self, payload: dict) -> Any:
|
||||
if "betas" in payload:
|
||||
return await self._async_client.beta.messages.create(**payload)
|
||||
else:
|
||||
return await self._async_client.messages.create(**payload)
|
||||
|
||||
def _stream(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
@@ -1121,17 +1335,19 @@ class ChatAnthropic(BaseChatModel):
|
||||
kwargs["stream"] = True
|
||||
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
||||
try:
|
||||
stream = self._client.messages.create(**payload)
|
||||
stream = self._create(payload)
|
||||
coerce_content_to_string = (
|
||||
not _tools_in_params(payload)
|
||||
and not _documents_in_params(payload)
|
||||
and not _thinking_in_params(payload)
|
||||
)
|
||||
block_start_event = None
|
||||
for event in stream:
|
||||
msg = _make_message_chunk_from_anthropic_event(
|
||||
msg, block_start_event = _make_message_chunk_from_anthropic_event(
|
||||
event,
|
||||
stream_usage=stream_usage,
|
||||
coerce_content_to_string=coerce_content_to_string,
|
||||
block_start_event=block_start_event,
|
||||
)
|
||||
if msg is not None:
|
||||
chunk = ChatGenerationChunk(message=msg)
|
||||
@@ -1155,17 +1371,19 @@ class ChatAnthropic(BaseChatModel):
|
||||
kwargs["stream"] = True
|
||||
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
||||
try:
|
||||
stream = await self._async_client.messages.create(**payload)
|
||||
stream = await self._acreate(payload)
|
||||
coerce_content_to_string = (
|
||||
not _tools_in_params(payload)
|
||||
and not _documents_in_params(payload)
|
||||
and not _thinking_in_params(payload)
|
||||
)
|
||||
block_start_event = None
|
||||
async for event in stream:
|
||||
msg = _make_message_chunk_from_anthropic_event(
|
||||
msg, block_start_event = _make_message_chunk_from_anthropic_event(
|
||||
event,
|
||||
stream_usage=stream_usage,
|
||||
coerce_content_to_string=coerce_content_to_string,
|
||||
block_start_event=block_start_event,
|
||||
)
|
||||
if msg is not None:
|
||||
chunk = ChatGenerationChunk(message=msg)
|
||||
@@ -1234,7 +1452,7 @@ class ChatAnthropic(BaseChatModel):
|
||||
return generate_from_stream(stream_iter)
|
||||
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
||||
try:
|
||||
data = self._client.messages.create(**payload)
|
||||
data = self._create(payload)
|
||||
except anthropic.BadRequestError as e:
|
||||
_handle_anthropic_bad_request(e)
|
||||
return self._format_output(data, **kwargs)
|
||||
@@ -1253,7 +1471,7 @@ class ChatAnthropic(BaseChatModel):
|
||||
return await agenerate_from_stream(stream_iter)
|
||||
payload = self._get_request_payload(messages, stop=stop, **kwargs)
|
||||
try:
|
||||
data = await self._async_client.messages.create(**payload)
|
||||
data = await self._acreate(payload)
|
||||
except anthropic.BadRequestError as e:
|
||||
_handle_anthropic_bad_request(e)
|
||||
return self._format_output(data, **kwargs)
|
||||
@@ -1722,8 +1940,10 @@ def convert_to_anthropic_tool(
|
||||
|
||||
|
||||
def _tools_in_params(params: dict) -> bool:
|
||||
return "tools" in params or (
|
||||
"extra_body" in params and params["extra_body"].get("tools")
|
||||
return (
|
||||
"tools" in params
|
||||
or ("extra_body" in params and params["extra_body"].get("tools"))
|
||||
or "mcp_servers" in params
|
||||
)
|
||||
|
||||
|
||||
@@ -1772,7 +1992,8 @@ def _make_message_chunk_from_anthropic_event(
|
||||
*,
|
||||
stream_usage: bool = True,
|
||||
coerce_content_to_string: bool,
|
||||
) -> Optional[AIMessageChunk]:
|
||||
block_start_event: Optional[anthropic.types.RawMessageStreamEvent] = None,
|
||||
) -> tuple[Optional[AIMessageChunk], Optional[anthropic.types.RawMessageStreamEvent]]:
|
||||
"""Convert Anthropic event to AIMessageChunk.
|
||||
|
||||
Note that not all events will result in a message chunk. In these cases
|
||||
@@ -1800,7 +2021,17 @@ def _make_message_chunk_from_anthropic_event(
|
||||
elif (
|
||||
event.type == "content_block_start"
|
||||
and event.content_block is not None
|
||||
and event.content_block.type in ("tool_use", "document", "redacted_thinking")
|
||||
and event.content_block.type
|
||||
in (
|
||||
"tool_use",
|
||||
"code_execution_tool_result",
|
||||
"document",
|
||||
"redacted_thinking",
|
||||
"mcp_tool_use",
|
||||
"mcp_tool_result",
|
||||
"server_tool_use",
|
||||
"web_search_tool_result",
|
||||
)
|
||||
):
|
||||
if coerce_content_to_string:
|
||||
warnings.warn("Received unexpected tool content block.")
|
||||
@@ -1820,6 +2051,7 @@ def _make_message_chunk_from_anthropic_event(
|
||||
content=[content_block],
|
||||
tool_call_chunks=tool_call_chunks, # type: ignore
|
||||
)
|
||||
block_start_event = event
|
||||
elif event.type == "content_block_delta":
|
||||
if event.delta.type in ("text_delta", "citations_delta"):
|
||||
if coerce_content_to_string and hasattr(event.delta, "text"):
|
||||
@@ -1849,16 +2081,23 @@ def _make_message_chunk_from_anthropic_event(
|
||||
elif event.delta.type == "input_json_delta":
|
||||
content_block = event.delta.model_dump()
|
||||
content_block["index"] = event.index
|
||||
content_block["type"] = "tool_use"
|
||||
tool_call_chunk = create_tool_call_chunk(
|
||||
index=event.index,
|
||||
id=None,
|
||||
name=None,
|
||||
args=event.delta.partial_json,
|
||||
)
|
||||
if (
|
||||
(block_start_event is not None)
|
||||
and hasattr(block_start_event, "content_block")
|
||||
and (block_start_event.content_block.type == "tool_use")
|
||||
):
|
||||
tool_call_chunk = create_tool_call_chunk(
|
||||
index=event.index,
|
||||
id=None,
|
||||
name=None,
|
||||
args=event.delta.partial_json,
|
||||
)
|
||||
tool_call_chunks = [tool_call_chunk]
|
||||
else:
|
||||
tool_call_chunks = []
|
||||
message_chunk = AIMessageChunk(
|
||||
content=[content_block],
|
||||
tool_call_chunks=[tool_call_chunk], # type: ignore
|
||||
tool_call_chunks=tool_call_chunks, # type: ignore
|
||||
)
|
||||
elif event.type == "message_delta" and stream_usage:
|
||||
usage_metadata = UsageMetadata(
|
||||
@@ -1877,7 +2116,7 @@ def _make_message_chunk_from_anthropic_event(
|
||||
else:
|
||||
pass
|
||||
|
||||
return message_chunk
|
||||
return message_chunk, block_start_event
|
||||
|
||||
|
||||
@deprecated(since="0.1.0", removal="1.0.0", alternative="ChatAnthropic")
|
||||
|
@@ -7,7 +7,7 @@ authors = []
|
||||
license = { text = "MIT" }
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"anthropic<1,>=0.51.0",
|
||||
"anthropic<1,>=0.52.0",
|
||||
"langchain-core<1.0.0,>=0.3.59",
|
||||
"pydantic<3.0.0,>=2.7.4",
|
||||
]
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""Test ChatAnthropic chat model."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from base64 import b64encode
|
||||
from typing import Optional
|
||||
|
||||
@@ -863,3 +864,206 @@ def test_image_tool_calling() -> None:
|
||||
]
|
||||
llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||
llm.bind_tools([color_picker]).invoke(messages)
|
||||
|
||||
|
||||
# TODO: set up VCR
|
||||
def test_web_search() -> None:
|
||||
pytest.skip()
|
||||
llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
|
||||
|
||||
tool = {"type": "web_search_20250305", "name": "web_search", "max_uses": 1}
|
||||
llm_with_tools = llm.bind_tools([tool])
|
||||
|
||||
input_message = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "How do I update a web app to TypeScript 5.5?",
|
||||
}
|
||||
],
|
||||
}
|
||||
response = llm_with_tools.invoke([input_message])
|
||||
block_types = {block["type"] for block in response.content}
|
||||
assert block_types == {"text", "server_tool_use", "web_search_tool_result"}
|
||||
|
||||
# Test streaming
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
for chunk in llm_with_tools.stream([input_message]):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
assert isinstance(full.content, list)
|
||||
block_types = {block["type"] for block in full.content} # type: ignore[index]
|
||||
assert block_types == {"text", "server_tool_use", "web_search_tool_result"}
|
||||
|
||||
# Test we can pass back in
|
||||
next_message = {
|
||||
"role": "user",
|
||||
"content": "Please repeat the last search, but focus on sources from 2024.",
|
||||
}
|
||||
_ = llm_with_tools.invoke(
|
||||
[input_message, full, next_message],
|
||||
)
|
||||
|
||||
|
||||
def test_code_execution() -> None:
|
||||
pytest.skip()
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-20250514",
|
||||
betas=["code-execution-2025-05-22"],
|
||||
)
|
||||
|
||||
tool = {"type": "code_execution_20250522", "name": "code_execution"}
|
||||
llm_with_tools = llm.bind_tools([tool])
|
||||
|
||||
input_message = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": (
|
||||
"Calculate the mean and standard deviation of "
|
||||
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
response = llm_with_tools.invoke([input_message])
|
||||
block_types = {block["type"] for block in response.content}
|
||||
assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}
|
||||
|
||||
# Test streaming
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
for chunk in llm_with_tools.stream([input_message]):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
assert isinstance(full.content, list)
|
||||
block_types = {block["type"] for block in full.content} # type: ignore[index]
|
||||
assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}
|
||||
|
||||
# Test we can pass back in
|
||||
next_message = {
|
||||
"role": "user",
|
||||
"content": "Please add more comments to the code.",
|
||||
}
|
||||
_ = llm_with_tools.invoke(
|
||||
[input_message, full, next_message],
|
||||
)
|
||||
|
||||
|
||||
def test_remote_mcp() -> None:
|
||||
pytest.skip()
|
||||
mcp_servers = [
|
||||
{
|
||||
"type": "url",
|
||||
"url": "https://mcp.deepwiki.com/mcp",
|
||||
"name": "deepwiki",
|
||||
"tool_configuration": {"enabled": True, "allowed_tools": ["ask_question"]},
|
||||
"authorization_token": "PLACEHOLDER",
|
||||
}
|
||||
]
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-20250514",
|
||||
betas=["mcp-client-2025-04-04"],
|
||||
mcp_servers=mcp_servers,
|
||||
)
|
||||
|
||||
input_message = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": (
|
||||
"What transport protocols does the 2025-03-26 version of the MCP "
|
||||
"spec (modelcontextprotocol/modelcontextprotocol) support?"
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
response = llm.invoke([input_message])
|
||||
block_types = {block["type"] for block in response.content}
|
||||
assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"}
|
||||
|
||||
# Test streaming
|
||||
full: Optional[BaseMessageChunk] = None
|
||||
for chunk in llm.stream([input_message]):
|
||||
assert isinstance(chunk, AIMessageChunk)
|
||||
full = chunk if full is None else full + chunk
|
||||
assert isinstance(full, AIMessageChunk)
|
||||
assert isinstance(full.content, list)
|
||||
block_types = {block["type"] for block in full.content}
|
||||
assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"}
|
||||
|
||||
# Test we can pass back in
|
||||
next_message = {
|
||||
"role": "user",
|
||||
"content": "Please query the same tool again, but add 'please' to your query.",
|
||||
}
|
||||
_ = llm.invoke(
|
||||
[input_message, full, next_message],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("block_format", ["anthropic", "standard"])
|
||||
def test_files_api_image(block_format: str) -> None:
|
||||
image_file_id = os.getenv("ANTHROPIC_FILES_API_IMAGE_ID")
|
||||
if not image_file_id:
|
||||
pytest.skip()
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-20250514",
|
||||
betas=["files-api-2025-04-14"],
|
||||
)
|
||||
if block_format == "anthropic":
|
||||
block = {
|
||||
"type": "image",
|
||||
"source": {
|
||||
"type": "file",
|
||||
"file_id": image_file_id,
|
||||
},
|
||||
}
|
||||
else:
|
||||
# standard block format
|
||||
block = {
|
||||
"type": "image",
|
||||
"source_type": "id",
|
||||
"id": image_file_id,
|
||||
}
|
||||
input_message = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Describe this image."},
|
||||
block,
|
||||
],
|
||||
}
|
||||
_ = llm.invoke([input_message])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("block_format", ["anthropic", "standard"])
|
||||
def test_files_api_pdf(block_format: str) -> None:
|
||||
pdf_file_id = os.getenv("ANTHROPIC_FILES_API_PDF_ID")
|
||||
if not pdf_file_id:
|
||||
pytest.skip()
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-20250514",
|
||||
betas=["files-api-2025-04-14"],
|
||||
)
|
||||
if block_format == "anthropic":
|
||||
block = {"type": "document", "source": {"type": "file", "file_id": pdf_file_id}}
|
||||
else:
|
||||
# standard block format
|
||||
block = {
|
||||
"type": "file",
|
||||
"source_type": "id",
|
||||
"id": pdf_file_id,
|
||||
}
|
||||
input_message = {
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "Describe this document."},
|
||||
block,
|
||||
],
|
||||
}
|
||||
_ = llm.invoke([input_message])
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
import os
|
||||
from typing import Any, Callable, Literal, Optional, cast
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import anthropic
|
||||
import pytest
|
||||
@@ -10,6 +10,8 @@ from anthropic.types import Message, TextBlock, Usage
|
||||
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolMessage
|
||||
from langchain_core.runnables import RunnableBinding
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain_core.tracers.base import BaseTracer
|
||||
from langchain_core.tracers.schemas import Run
|
||||
from pydantic import BaseModel, Field, SecretStr
|
||||
from pytest import CaptureFixture, MonkeyPatch
|
||||
|
||||
@@ -994,3 +996,63 @@ def test_usage_metadata_standardization() -> None:
|
||||
assert result["input_tokens"] == 0
|
||||
assert result["output_tokens"] == 0
|
||||
assert result["total_tokens"] == 0
|
||||
|
||||
|
||||
class FakeTracer(BaseTracer):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.chat_model_start_inputs: list = []
|
||||
|
||||
def _persist_run(self, run: Run) -> None:
|
||||
"""Persist a run."""
|
||||
pass
|
||||
|
||||
def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:
|
||||
self.chat_model_start_inputs.append({"args": args, "kwargs": kwargs})
|
||||
return super().on_chat_model_start(*args, **kwargs)
|
||||
|
||||
|
||||
def test_mcp_tracing() -> None:
|
||||
# Test we exclude sensitive information from traces
|
||||
mcp_servers = [
|
||||
{
|
||||
"type": "url",
|
||||
"url": "https://mcp.deepwiki.com/mcp",
|
||||
"name": "deepwiki",
|
||||
"authorization_token": "PLACEHOLDER",
|
||||
}
|
||||
]
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-20250514",
|
||||
betas=["mcp-client-2025-04-04"],
|
||||
mcp_servers=mcp_servers,
|
||||
)
|
||||
|
||||
tracer = FakeTracer()
|
||||
mock_client = MagicMock()
|
||||
|
||||
def mock_create(*args: Any, **kwargs: Any) -> Message:
|
||||
return Message(
|
||||
id="foo",
|
||||
content=[TextBlock(type="text", text="bar")],
|
||||
model="baz",
|
||||
role="assistant",
|
||||
stop_reason=None,
|
||||
stop_sequence=None,
|
||||
usage=Usage(input_tokens=2, output_tokens=1),
|
||||
type="message",
|
||||
)
|
||||
|
||||
mock_client.messages.create = mock_create
|
||||
input_message = HumanMessage("Test query")
|
||||
with patch.object(llm, "_client", mock_client):
|
||||
_ = llm.invoke([input_message], config={"callbacks": [tracer]})
|
||||
|
||||
# Test headers are not traced
|
||||
assert len(tracer.chat_model_start_inputs) == 1
|
||||
assert "PLACEHOLDER" not in str(tracer.chat_model_start_inputs)
|
||||
|
||||
# Test headers are correctly propagated to request
|
||||
payload = llm._get_request_payload([input_message])
|
||||
assert payload["mcp_servers"][0]["authorization_token"] == "PLACEHOLDER"
|
||||
|
15
libs/partners/anthropic/uv.lock
generated
15
libs/partners/anthropic/uv.lock
generated
@@ -18,7 +18,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.51.0"
|
||||
version = "0.52.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -29,9 +29,9 @@ dependencies = [
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/4a/96f99a61ae299f9e5aa3e765d7342d95ab2e2ba5b69a3ffedb00ef779651/anthropic-0.51.0.tar.gz", hash = "sha256:6f824451277992af079554430d5b2c8ff5bc059cc2c968cdc3f06824437da201", size = 219063 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/fd/8a9332f5baf352c272494a9d359863a53385a208954c1a7251a524071930/anthropic-0.52.0.tar.gz", hash = "sha256:f06bc924d7eb85f8a43fe587b875ff58b410d60251b7dc5f1387b322a35bd67b", size = 229372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/6e/9637122c5f007103bd5a259f4250bd8f1533dd2473227670fd10a1457b62/anthropic-0.51.0-py3-none-any.whl", hash = "sha256:b8b47d482c9aa1f81b923555cebb687c2730309a20d01be554730c8302e0f62a", size = 263957 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/43/172c0031654908bbac2a87d356fff4de1b4947a9b14b9658540b69416417/anthropic-0.52.0-py3-none-any.whl", hash = "sha256:c026daa164f0e3bde36ce9cbdd27f5f1419fff03306be1e138726f42e6a7810f", size = 286076 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -451,7 +451,7 @@ typing = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anthropic", specifier = ">=0.51.0,<1" },
|
||||
{ name = "anthropic", specifier = ">=0.52.0,<1" },
|
||||
{ name = "langchain-core", editable = "../../core" },
|
||||
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
|
||||
]
|
||||
@@ -486,7 +486,7 @@ typing = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "0.3.59"
|
||||
version = "0.3.61"
|
||||
source = { editable = "../../core" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -501,10 +501,9 @@ dependencies = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "jsonpatch", specifier = ">=1.33,<2.0" },
|
||||
{ name = "langsmith", specifier = ">=0.1.125,<0.4" },
|
||||
{ name = "langsmith", specifier = ">=0.1.126,<0.4" },
|
||||
{ name = "packaging", specifier = ">=23.2,<25" },
|
||||
{ name = "pydantic", marker = "python_full_version < '3.12.4'", specifier = ">=2.5.2,<3.0.0" },
|
||||
{ name = "pydantic", marker = "python_full_version >= '3.12.4'", specifier = ">=2.7.4,<3.0.0" },
|
||||
{ name = "pydantic", specifier = ">=2.7.4" },
|
||||
{ name = "pyyaml", specifier = ">=5.3" },
|
||||
{ name = "tenacity", specifier = ">=8.1.0,!=8.4.0,<10.0.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.7" },
|
||||
|
Reference in New Issue
Block a user