mirror of
https://github.com/hwchase17/langchain.git
synced 2026-05-07 03:59:39 +00:00
feat(anthropic): support memory and context management features (#33146)
https://docs.claude.com/en/docs/build-with-claude/context-editing --------- Co-authored-by: Mason Daugherty <mason@langchain.dev>
This commit is contained in:
@@ -12,7 +12,7 @@ from operator import itemgetter
|
||||
from typing import Any, Callable, Literal, Optional, Union, cast
|
||||
|
||||
import anthropic
|
||||
from langchain_core._api import beta, deprecated
|
||||
from langchain_core._api import deprecated
|
||||
from langchain_core.callbacks import (
|
||||
AsyncCallbackManagerForLLMRun,
|
||||
CallbackManagerForLLMRun,
|
||||
@@ -91,6 +91,7 @@ def _is_builtin_tool(tool: Any) -> bool:
|
||||
"web_search_",
|
||||
"web_fetch_",
|
||||
"code_execution_",
|
||||
"memory_",
|
||||
]
|
||||
return any(tool_type.startswith(prefix) for prefix in _builtin_tool_prefixes)
|
||||
|
||||
@@ -1193,6 +1194,25 @@ class ChatAnthropic(BaseChatModel):
|
||||
|
||||
Total tokens: 408
|
||||
|
||||
Context management:
|
||||
Anthropic supports a context editing feature that will automatically manage the
|
||||
model's context window (e.g., by clearing tool results).
|
||||
|
||||
See `Anthropic documentation <https://docs.claude.com/en/docs/build-with-claude/context-editing>`__
|
||||
for details and configuration options.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from langchain_anthropic import ChatAnthropic
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
betas=["context-management-2025-06-27"],
|
||||
context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},
|
||||
)
|
||||
llm_with_tools = llm.bind_tools([{"type": "web_search_20250305", "name": "web_search"}])
|
||||
response = llm_with_tools.invoke("Search for recent developments in AI")
|
||||
|
||||
Built-in tools:
|
||||
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/#built-in-tools>`__
|
||||
for more detail.
|
||||
@@ -1413,6 +1433,11 @@ class ChatAnthropic(BaseChatModel):
|
||||
"name": "example-mcp"}]``
|
||||
"""
|
||||
|
||||
context_management: Optional[dict[str, Any]] = None
|
||||
"""Configuration for
|
||||
`context management <https://docs.claude.com/en/docs/build-with-claude/context-editing>`__.
|
||||
"""
|
||||
|
||||
@property
|
||||
def _llm_type(self) -> str:
|
||||
"""Return type of chat model."""
|
||||
@@ -1565,6 +1590,7 @@ class ChatAnthropic(BaseChatModel):
|
||||
"top_p": self.top_p,
|
||||
"stop_sequences": stop or self.stop_sequences,
|
||||
"betas": self.betas,
|
||||
"context_management": self.context_management,
|
||||
"mcp_servers": self.mcp_servers,
|
||||
"system": system,
|
||||
**self.model_kwargs,
|
||||
@@ -2219,7 +2245,6 @@ class ChatAnthropic(BaseChatModel):
|
||||
return RunnableMap(raw=llm) | parser_with_fallback
|
||||
return llm | output_parser
|
||||
|
||||
@beta()
|
||||
def get_num_tokens_from_messages(
|
||||
self,
|
||||
messages: list[BaseMessage],
|
||||
@@ -2234,8 +2259,8 @@ class ChatAnthropic(BaseChatModel):
|
||||
messages: The message inputs to tokenize.
|
||||
tools: If provided, sequence of dict, BaseModel, function, or BaseTools
|
||||
to be converted to tool schemas.
|
||||
kwargs: Additional keyword arguments are passed to the
|
||||
:meth:`~langchain_anthropic.chat_models.ChatAnthropic.bind` method.
|
||||
kwargs: Additional keyword arguments are passed to the Anthropic
|
||||
``messages.count_tokens`` method.
|
||||
|
||||
Basic usage:
|
||||
|
||||
@@ -2270,7 +2295,7 @@ class ChatAnthropic(BaseChatModel):
|
||||
def get_weather(location: str) -> str:
|
||||
\"\"\"Get the current weather in a given location
|
||||
|
||||
Args:
|
||||
Args:
|
||||
location: The city and state, e.g. San Francisco, CA
|
||||
\"\"\"
|
||||
return "Sunny"
|
||||
@@ -2288,15 +2313,24 @@ class ChatAnthropic(BaseChatModel):
|
||||
|
||||
Uses Anthropic's `token counting API <https://docs.anthropic.com/en/docs/build-with-claude/token-counting>`__ to count tokens in messages.
|
||||
|
||||
""" # noqa: E501
|
||||
""" # noqa: D214,E501
|
||||
formatted_system, formatted_messages = _format_messages(messages)
|
||||
if isinstance(formatted_system, str):
|
||||
kwargs["system"] = formatted_system
|
||||
if tools:
|
||||
kwargs["tools"] = [convert_to_anthropic_tool(tool) for tool in tools]
|
||||
if self.context_management is not None:
|
||||
kwargs["context_management"] = self.context_management
|
||||
|
||||
response = self._client.beta.messages.count_tokens(
|
||||
betas=["token-counting-2024-11-01"],
|
||||
if self.betas is not None:
|
||||
beta_response = self._client.beta.messages.count_tokens(
|
||||
betas=self.betas,
|
||||
model=self.model,
|
||||
messages=formatted_messages, # type: ignore[arg-type]
|
||||
**kwargs,
|
||||
)
|
||||
return beta_response.input_tokens
|
||||
response = self._client.messages.count_tokens(
|
||||
model=self.model,
|
||||
messages=formatted_messages, # type: ignore[arg-type]
|
||||
**kwargs,
|
||||
@@ -2409,7 +2443,7 @@ def _make_message_chunk_from_anthropic_event(
|
||||
# Capture model name, but don't include usage_metadata yet
|
||||
# as it will be properly reported in message_delta with complete info
|
||||
if hasattr(event.message, "model"):
|
||||
response_metadata = {"model_name": event.message.model}
|
||||
response_metadata: dict[str, Any] = {"model_name": event.message.model}
|
||||
else:
|
||||
response_metadata = {}
|
||||
|
||||
@@ -2510,13 +2544,16 @@ def _make_message_chunk_from_anthropic_event(
|
||||
# Process final usage metadata and completion info
|
||||
elif event.type == "message_delta" and stream_usage:
|
||||
usage_metadata = _create_usage_metadata(event.usage)
|
||||
response_metadata = {
|
||||
"stop_reason": event.delta.stop_reason,
|
||||
"stop_sequence": event.delta.stop_sequence,
|
||||
}
|
||||
if context_management := getattr(event, "context_management", None):
|
||||
response_metadata["context_management"] = context_management.model_dump()
|
||||
message_chunk = AIMessageChunk(
|
||||
content="",
|
||||
usage_metadata=usage_metadata,
|
||||
response_metadata={
|
||||
"stop_reason": event.delta.stop_reason,
|
||||
"stop_sequence": event.delta.stop_sequence,
|
||||
},
|
||||
response_metadata=response_metadata,
|
||||
)
|
||||
# Unhandled event types (e.g., `content_block_stop`, `ping` events)
|
||||
# https://docs.anthropic.com/en/docs/build-with-claude/streaming#other-events
|
||||
|
||||
@@ -7,7 +7,7 @@ authors = []
|
||||
license = { text = "MIT" }
|
||||
requires-python = ">=3.9.0,<4.0.0"
|
||||
dependencies = [
|
||||
"anthropic>=0.67.0,<1.0.0",
|
||||
"anthropic>=0.69.0,<1.0.0",
|
||||
"langchain-core>=0.3.76,<2.0.0",
|
||||
"pydantic>=2.7.4,<3.0.0",
|
||||
]
|
||||
|
||||
Binary file not shown.
@@ -1485,6 +1485,50 @@ def test_search_result_top_level() -> None:
|
||||
assert any("citations" in block for block in result.content)
|
||||
|
||||
|
||||
def test_memory_tool() -> None:
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-5-20250929", # type: ignore[call-arg]
|
||||
betas=["context-management-2025-06-27"],
|
||||
)
|
||||
llm_with_tools = llm.bind_tools([{"type": "memory_20250818", "name": "memory"}])
|
||||
response = llm_with_tools.invoke("What are my interests?")
|
||||
assert isinstance(response, AIMessage)
|
||||
assert response.tool_calls
|
||||
assert response.tool_calls[0]["name"] == "memory"
|
||||
|
||||
|
||||
@pytest.mark.vcr
|
||||
def test_context_management() -> None:
|
||||
# TODO: update example to trigger action
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-5-20250929", # type: ignore[call-arg]
|
||||
betas=["context-management-2025-06-27"],
|
||||
context_management={
|
||||
"edits": [
|
||||
{
|
||||
"type": "clear_tool_uses_20250919",
|
||||
"trigger": {"type": "input_tokens", "value": 10},
|
||||
"clear_at_least": {"type": "input_tokens", "value": 5},
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
llm_with_tools = llm.bind_tools(
|
||||
[{"type": "web_search_20250305", "name": "web_search"}]
|
||||
)
|
||||
input_message = {"role": "user", "content": "Search for recent developments in AI"}
|
||||
response = llm_with_tools.invoke([input_message])
|
||||
assert response.response_metadata.get("context_management")
|
||||
|
||||
# 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 full.response_metadata.get("context_management")
|
||||
|
||||
|
||||
def test_async_shared_client() -> None:
|
||||
llm = ChatAnthropic(model="claude-3-5-haiku-latest") # type: ignore[call-arg]
|
||||
_ = asyncio.run(llm.ainvoke("Hello"))
|
||||
|
||||
@@ -1065,9 +1065,21 @@ def test_get_num_tokens_from_messages_passes_kwargs() -> None:
|
||||
with patch.object(anthropic, "Client") as _client:
|
||||
llm.get_num_tokens_from_messages([HumanMessage("foo")], foo="bar")
|
||||
|
||||
assert (
|
||||
_client.return_value.beta.messages.count_tokens.call_args.kwargs["foo"] == "bar"
|
||||
assert _client.return_value.messages.count_tokens.call_args.kwargs["foo"] == "bar"
|
||||
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-5-20250929",
|
||||
betas=["context-management-2025-06-27"],
|
||||
context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},
|
||||
)
|
||||
with patch.object(anthropic, "Client") as _client:
|
||||
llm.get_num_tokens_from_messages([HumanMessage("foo")])
|
||||
|
||||
call_args = _client.return_value.beta.messages.count_tokens.call_args.kwargs
|
||||
assert call_args["betas"] == ["context-management-2025-06-27"]
|
||||
assert call_args["context_management"] == {
|
||||
"edits": [{"type": "clear_tool_uses_20250919"}]
|
||||
}
|
||||
|
||||
|
||||
def test_usage_metadata_standardization() -> None:
|
||||
@@ -1217,6 +1229,22 @@ def test_cache_control_kwarg() -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_context_management_in_payload() -> None:
|
||||
llm = ChatAnthropic(
|
||||
model="claude-sonnet-4-5-20250929", # type: ignore[call-arg]
|
||||
betas=["context-management-2025-06-27"],
|
||||
context_management={"edits": [{"type": "clear_tool_uses_20250919"}]},
|
||||
)
|
||||
llm_with_tools = llm.bind_tools(
|
||||
[{"type": "web_search_20250305", "name": "web_search"}]
|
||||
)
|
||||
input_message = HumanMessage("Search for recent developments in AI")
|
||||
payload = llm_with_tools._get_request_payload([input_message]) # type: ignore[attr-defined]
|
||||
assert payload["context_management"] == {
|
||||
"edits": [{"type": "clear_tool_uses_20250919"}]
|
||||
}
|
||||
|
||||
|
||||
def test_anthropic_model_params() -> None:
|
||||
llm = ChatAnthropic(model="claude-3-5-haiku-latest")
|
||||
|
||||
|
||||
20
libs/partners/anthropic/uv.lock
generated
20
libs/partners/anthropic/uv.lock
generated
@@ -1,5 +1,5 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = ">=3.9.0, <4.0.0"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13' and platform_python_implementation == 'PyPy'",
|
||||
@@ -21,20 +21,21 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.67.0"
|
||||
version = "0.69.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "docstring-parser" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/08/ee91464cd821e6fca52d9a23be44815c95edd3c1cf1e844b2c5e85f0d57f/anthropic-0.67.0.tar.gz", hash = "sha256:d1531b210ea300c73423141d29bcee20fcd24ef9f426f6437c0a5d93fc98fb8e", size = 441639, upload-time = "2025-09-10T14:47:18.137Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c8/9d/9ad1778b95f15c5b04e7d328c1b5f558f1e893857b7c33cd288c19c0057a/anthropic-0.69.0.tar.gz", hash = "sha256:c604d287f4d73640f40bd2c0f3265a2eb6ce034217ead0608f6b07a8bc5ae5f2", size = 480622, upload-time = "2025-09-29T16:53:45.282Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/9d/9adbda372710918cc8271d089a2ceae4d977a125f90bc3c4b456bca4f281/anthropic-0.67.0-py3-none-any.whl", hash = "sha256:f80a81ec1132c514215f33d25edeeab1c4691ad5361b391ebb70d528b0605b55", size = 317126, upload-time = "2025-09-10T14:47:16.351Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/38/75129688de5637eb5b383e5f2b1570a5cc3aecafa4de422da8eea4b90a6c/anthropic-0.69.0-py3-none-any.whl", hash = "sha256:1f73193040f33f11e27c2cd6ec25f24fe7c3f193dc1c5cde6b7a08b18a16bcc5", size = 337265, upload-time = "2025-09-29T16:53:43.686Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -257,6 +258,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser"
|
||||
version = "0.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
@@ -497,7 +507,7 @@ typing = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "anthropic", specifier = ">=0.67.0,<1.0.0" },
|
||||
{ name = "anthropic", specifier = ">=0.69.0,<1.0.0" },
|
||||
{ name = "langchain-core", editable = "../../core" },
|
||||
{ name = "pydantic", specifier = ">=2.7.4,<3.0.0" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user