release: v1.0.0 (#32567)

Co-authored-by: Mohammad Mohtashim <45242107+keenborder786@users.noreply.github.com>
Co-authored-by: Caspar Broekhuizen <caspar@langchain.dev>
Co-authored-by: ccurme <chester.curme@gmail.com>
Co-authored-by: Christophe Bornet <cbornet@hotmail.com>
Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
Co-authored-by: Sadra Barikbin <sadraqazvin1@yahoo.com>
Co-authored-by: Vadym Barda <vadim.barda@gmail.com>
This commit is contained in:
Mason Daugherty
2025-10-02 10:49:42 -04:00
committed by GitHub
parent d7cce2f469
commit eaa6dcce9e
188 changed files with 23644 additions and 17479 deletions

View File

@@ -6,7 +6,7 @@ import asyncio
import json
import os
from base64 import b64encode
from typing import Optional, cast
from typing import Literal, Optional, cast
import httpx
import pytest
@@ -28,16 +28,17 @@ from langchain_core.prompts import ChatPromptTemplate
from langchain_core.tools import tool
from pydantic import BaseModel, Field
from langchain_anthropic import ChatAnthropic, ChatAnthropicMessages
from langchain_anthropic import ChatAnthropic
from langchain_anthropic._compat import _convert_from_v1_to_anthropic
from tests.unit_tests._utils import FakeCallbackHandler
MODEL_NAME = "claude-opus-4-1-20250805"
IMAGE_MODEL_NAME = "claude-opus-4-1-20250805"
MODEL_NAME = "claude-3-5-haiku-latest"
IMAGE_MODEL_NAME = "claude-3-5-haiku-latest"
def test_stream() -> None:
"""Test streaming tokens from Anthropic."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
full: Optional[BaseMessageChunk] = None
chunks_with_input_token_counts = 0
@@ -65,6 +66,9 @@ def test_stream() -> None:
assert chunks_with_model_name == 1
# check token usage is populated
assert isinstance(full, AIMessageChunk)
assert len(full.content_blocks) == 1
assert full.content_blocks[0]["type"] == "text"
assert full.content_blocks[0]["text"]
assert full.usage_metadata is not None
assert full.usage_metadata["input_tokens"] > 0
assert full.usage_metadata["output_tokens"] > 0
@@ -80,7 +84,7 @@ def test_stream() -> None:
async def test_astream() -> None:
"""Test streaming tokens from Anthropic."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
full: Optional[BaseMessageChunk] = None
chunks_with_input_token_counts = 0
@@ -105,6 +109,9 @@ async def test_astream() -> None:
)
# check token usage is populated
assert isinstance(full, AIMessageChunk)
assert len(full.content_blocks) == 1
assert full.content_blocks[0]["type"] == "text"
assert full.content_blocks[0]["text"]
assert full.usage_metadata is not None
assert full.usage_metadata["input_tokens"] > 0
assert full.usage_metadata["output_tokens"] > 0
@@ -155,8 +162,8 @@ async def test_stream_usage_override() -> None:
async def test_abatch() -> None:
"""Test streaming tokens from ChatAnthropicMessages."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
"""Test streaming tokens from ChatAnthropic."""
llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"])
for token in result:
@@ -164,8 +171,8 @@ async def test_abatch() -> None:
async def test_abatch_tags() -> None:
"""Test batch tokens from ChatAnthropicMessages."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
"""Test batch tokens from ChatAnthropic."""
llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = await llm.abatch(
["I'm Pickle Rick", "I'm not Pickle Rick"],
@@ -225,8 +232,8 @@ async def test_async_tool_use() -> None:
def test_batch() -> None:
"""Test batch tokens from ChatAnthropicMessages."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
"""Test batch tokens from ChatAnthropic."""
llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"])
for token in result:
@@ -234,8 +241,8 @@ def test_batch() -> None:
async def test_ainvoke() -> None:
"""Test invoke tokens from ChatAnthropicMessages."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
"""Test invoke tokens from ChatAnthropic."""
llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]})
assert isinstance(result.content, str)
@@ -243,8 +250,8 @@ async def test_ainvoke() -> None:
def test_invoke() -> None:
"""Test invoke tokens from ChatAnthropicMessages."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
"""Test invoke tokens from ChatAnthropic."""
llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]})
assert isinstance(result.content, str)
@@ -252,7 +259,7 @@ def test_invoke() -> None:
def test_system_invoke() -> None:
"""Test invoke tokens with a system message."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
llm = ChatAnthropic(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
prompt = ChatPromptTemplate.from_messages(
[
@@ -369,7 +376,7 @@ def test_streaming() -> None:
callback_handler = FakeCallbackHandler()
callback_manager = CallbackManager([callback_handler])
llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg]
llm = ChatAnthropic( # type: ignore[call-arg, call-arg]
model_name=MODEL_NAME,
streaming=True,
callback_manager=callback_manager,
@@ -385,7 +392,7 @@ async def test_astreaming() -> None:
callback_handler = FakeCallbackHandler()
callback_manager = CallbackManager([callback_handler])
llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg]
llm = ChatAnthropic( # type: ignore[call-arg, call-arg]
model_name=MODEL_NAME,
streaming=True,
callback_manager=callback_manager,
@@ -421,6 +428,14 @@ def test_tool_use() -> None:
assert isinstance(tool_call["args"], dict)
assert "location" in tool_call["args"]
content_blocks = response.content_blocks
assert len(content_blocks) == 2
assert content_blocks[0]["type"] == "text"
assert content_blocks[0]["text"]
assert content_blocks[1]["type"] == "tool_call"
assert content_blocks[1]["name"] == "get_weather"
assert content_blocks[1]["args"] == tool_call["args"]
# Test streaming
llm = ChatAnthropic(
model="claude-3-7-sonnet-20250219", # type: ignore[call-arg]
@@ -440,6 +455,8 @@ def test_tool_use() -> None:
first = False
else:
gathered = gathered + chunk # type: ignore[assignment]
for block in chunk.content_blocks:
assert block["type"] in ("text", "tool_call_chunk")
assert len(chunks) > 1
assert isinstance(gathered.content, list)
assert len(gathered.content) == 2
@@ -461,6 +478,14 @@ def test_tool_use() -> None:
assert "location" in tool_call["args"]
assert tool_call["id"] is not None
content_blocks = gathered.content_blocks
assert len(content_blocks) == 2
assert content_blocks[0]["type"] == "text"
assert content_blocks[0]["text"]
assert content_blocks[1]["type"] == "tool_call"
assert content_blocks[1]["name"] == "get_weather"
assert content_blocks[1]["args"]
# Testing token-efficient tools
# https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/token-efficient-tool-use
assert gathered.usage_metadata
@@ -500,6 +525,13 @@ def test_builtin_tools() -> None:
assert isinstance(response, AIMessage)
assert response.tool_calls
content_blocks = response.content_blocks
assert len(content_blocks) == 2
assert content_blocks[0]["type"] == "text"
assert content_blocks[0]["text"]
assert content_blocks[1]["type"] == "tool_call"
assert content_blocks[1]["name"] == "str_replace_editor"
class GenerateUsername(BaseModel):
"""Get a username based on someone's name and hair color."""
@@ -682,8 +714,74 @@ def test_pdf_document_input() -> None:
assert len(result.content) > 0
def test_citations() -> None:
llm = ChatAnthropic(model="claude-3-5-haiku-latest") # type: ignore[call-arg]
@pytest.mark.default_cassette("test_agent_loop.yaml.gz")
@pytest.mark.vcr
@pytest.mark.parametrize("output_version", ["v0", "v1"])
def test_agent_loop(output_version: Literal["v0", "v1"]) -> None:
@tool
def get_weather(location: str) -> str:
"""Get the weather for a location."""
return "It's sunny."
llm = ChatAnthropic(model="claude-3-5-haiku-latest", output_version=output_version) # type: ignore[call-arg]
llm_with_tools = llm.bind_tools([get_weather])
input_message = HumanMessage("What is the weather in San Francisco, CA?")
tool_call_message = llm_with_tools.invoke([input_message])
assert isinstance(tool_call_message, AIMessage)
tool_calls = tool_call_message.tool_calls
assert len(tool_calls) == 1
tool_call = tool_calls[0]
tool_message = get_weather.invoke(tool_call)
assert isinstance(tool_message, ToolMessage)
response = llm_with_tools.invoke(
[
input_message,
tool_call_message,
tool_message,
]
)
assert isinstance(response, AIMessage)
@pytest.mark.default_cassette("test_agent_loop_streaming.yaml.gz")
@pytest.mark.vcr
@pytest.mark.parametrize("output_version", ["v0", "v1"])
def test_agent_loop_streaming(output_version: Literal["v0", "v1"]) -> None:
@tool
def get_weather(location: str) -> str:
"""Get the weather for a location."""
return "It's sunny."
llm = ChatAnthropic(
model="claude-3-5-haiku-latest",
streaming=True,
output_version=output_version, # type: ignore[call-arg]
)
llm_with_tools = llm.bind_tools([get_weather])
input_message = HumanMessage("What is the weather in San Francisco, CA?")
tool_call_message = llm_with_tools.invoke([input_message])
assert isinstance(tool_call_message, AIMessage)
tool_calls = tool_call_message.tool_calls
assert len(tool_calls) == 1
tool_call = tool_calls[0]
tool_message = get_weather.invoke(tool_call)
assert isinstance(tool_message, ToolMessage)
response = llm_with_tools.invoke(
[
input_message,
tool_call_message,
tool_message,
]
)
assert isinstance(response, AIMessage)
@pytest.mark.default_cassette("test_citations.yaml.gz")
@pytest.mark.vcr
@pytest.mark.parametrize("output_version", ["v0", "v1"])
def test_citations(output_version: Literal["v0", "v1"]) -> None:
llm = ChatAnthropic(model="claude-3-5-haiku-latest", output_version=output_version) # type: ignore[call-arg]
messages = [
{
"role": "user",
@@ -706,7 +804,10 @@ def test_citations() -> None:
response = llm.invoke(messages)
assert isinstance(response, AIMessage)
assert isinstance(response.content, list)
assert any("citations" in block for block in response.content)
if output_version == "v1":
assert any("annotations" in block for block in response.content)
else:
assert any("citations" in block for block in response.content)
# Test streaming
full: Optional[BaseMessageChunk] = None
@@ -714,8 +815,11 @@ def test_citations() -> None:
full = cast("BaseMessageChunk", chunk) if full is None else full + chunk
assert isinstance(full, AIMessageChunk)
assert isinstance(full.content, list)
assert any("citations" in block for block in full.content)
assert not any("citation" in block for block in full.content)
if output_version == "v1":
assert any("annotations" in block for block in full.content)
else:
assert any("citations" in block for block in full.content)
# Test pass back in
next_message = {
@@ -766,26 +870,82 @@ def test_thinking() -> None:
_ = llm.invoke([input_message, full, next_message])
@pytest.mark.default_cassette("test_thinking.yaml.gz")
@pytest.mark.vcr
def test_redacted_thinking() -> None:
def test_thinking_v1() -> None:
llm = ChatAnthropic(
model="claude-3-7-sonnet-latest", # type: ignore[call-arg]
max_tokens=5_000, # type: ignore[call-arg]
thinking={"type": "enabled", "budget_tokens": 2_000},
output_version="v1",
)
input_message = {"role": "user", "content": "Hello"}
response = llm.invoke([input_message])
assert any("reasoning" in block for block in response.content)
for block in response.content:
assert isinstance(block, dict)
if block["type"] == "reasoning":
assert set(block.keys()) == {"type", "reasoning", "extras"}
assert block["reasoning"]
assert isinstance(block["reasoning"], str)
signature = block["extras"]["signature"]
assert signature
assert isinstance(signature, str)
# Test streaming
full: Optional[BaseMessageChunk] = None
for chunk in llm.stream([input_message]):
full = cast(BaseMessageChunk, chunk) if full is None else full + chunk
assert isinstance(full, AIMessageChunk)
assert isinstance(full.content, list)
assert any("reasoning" in block for block in full.content)
for block in full.content:
assert isinstance(block, dict)
if block["type"] == "reasoning":
assert set(block.keys()) == {"type", "reasoning", "extras", "index"}
assert block["reasoning"]
assert isinstance(block["reasoning"], str)
signature = block["extras"]["signature"]
assert signature
assert isinstance(signature, str)
# Test pass back in
next_message = {"role": "user", "content": "How are you?"}
_ = llm.invoke([input_message, full, next_message])
@pytest.mark.default_cassette("test_redacted_thinking.yaml.gz")
@pytest.mark.vcr
@pytest.mark.parametrize("output_version", ["v0", "v1"])
def test_redacted_thinking(output_version: Literal["v0", "v1"]) -> None:
llm = ChatAnthropic(
model="claude-3-7-sonnet-latest", # type: ignore[call-arg]
max_tokens=5_000, # type: ignore[call-arg]
thinking={"type": "enabled", "budget_tokens": 2_000},
output_version=output_version,
)
query = "ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB" # noqa: E501
input_message = {"role": "user", "content": query}
response = llm.invoke([input_message])
has_reasoning = False
value = None
for block in response.content:
assert isinstance(block, dict)
if block["type"] == "redacted_thinking":
has_reasoning = True
assert set(block.keys()) == {"type", "data"}
assert block["data"]
assert isinstance(block["data"], str)
assert has_reasoning
value = block
elif (
block["type"] == "non_standard"
and block["value"]["type"] == "redacted_thinking"
):
value = block["value"]
else:
pass
if value:
assert set(value.keys()) == {"type", "data"}
assert value["data"]
assert isinstance(value["data"], str)
assert value is not None
# Test streaming
full: Optional[BaseMessageChunk] = None
@@ -793,15 +953,27 @@ def test_redacted_thinking() -> None:
full = cast("BaseMessageChunk", chunk) if full is None else full + chunk
assert isinstance(full, AIMessageChunk)
assert isinstance(full.content, list)
stream_has_reasoning = False
value = None
for block in full.content:
assert isinstance(block, dict)
if block["type"] == "redacted_thinking":
stream_has_reasoning = True
assert set(block.keys()) == {"type", "data", "index"}
assert block["data"]
assert isinstance(block["data"], str)
assert stream_has_reasoning
value = block
assert set(value.keys()) == {"type", "data", "index"}
assert "index" in block
elif (
block["type"] == "non_standard"
and block["value"]["type"] == "redacted_thinking"
):
value = block["value"]
assert isinstance(value, dict)
assert set(value.keys()) == {"type", "data"}
assert "index" in block
else:
pass
if value:
assert value["data"]
assert isinstance(value["data"], str)
assert value is not None
# Test pass back in
next_message = {"role": "user", "content": "What?"}
@@ -905,9 +1077,15 @@ def test_image_tool_calling() -> None:
llm.bind_tools([color_picker]).invoke(messages)
@pytest.mark.default_cassette("test_web_search.yaml.gz")
@pytest.mark.vcr
def test_web_search() -> None:
llm = ChatAnthropic(model="claude-3-5-haiku-latest") # type: ignore[call-arg]
@pytest.mark.parametrize("output_version", ["v0", "v1"])
def test_web_search(output_version: Literal["v0", "v1"]) -> None:
llm = ChatAnthropic(
model="claude-3-5-haiku-latest", # type: ignore[call-arg]
max_tokens=1024,
output_version=output_version,
)
tool = {"type": "web_search_20250305", "name": "web_search", "max_uses": 1}
llm_with_tools = llm.bind_tools([tool])
@@ -924,17 +1102,24 @@ def test_web_search() -> None:
response = llm_with_tools.invoke([input_message])
assert all(isinstance(block, dict) for block in response.content)
block_types = {block["type"] for block in response.content} # type: ignore[index]
assert block_types == {"text", "server_tool_use", "web_search_tool_result"}
if output_version == "v0":
assert block_types == {"text", "server_tool_use", "web_search_tool_result"}
else:
assert block_types == {"text", "server_tool_call", "server_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"}
if output_version == "v0":
assert block_types == {"text", "server_tool_use", "web_search_tool_result"}
else:
assert block_types == {"text", "server_tool_call", "server_tool_result"}
# Test we can pass back in
next_message = {
@@ -946,13 +1131,79 @@ def test_web_search() -> None:
)
@pytest.mark.default_cassette("test_web_fetch_v1.yaml.gz")
@pytest.mark.vcr
@pytest.mark.parametrize("output_version", ["v0", "v1"])
def test_web_fetch_v1(output_version: Literal["v0", "v1"]) -> None:
"""Test that http calls are unchanged between v0 and v1."""
llm = ChatAnthropic(
model="claude-3-5-haiku-latest", # type: ignore[call-arg]
betas=["web-fetch-2025-09-10"],
output_version=output_version,
)
if output_version == "v0":
call_key = "server_tool_use"
result_key = "web_fetch_tool_result"
else:
# v1
call_key = "server_tool_call"
result_key = "server_tool_result"
tool = {
"type": "web_fetch_20250910",
"name": "web_fetch",
"max_uses": 1,
"citations": {"enabled": True},
}
llm_with_tools = llm.bind_tools([tool])
input_message = {
"role": "user",
"content": [
{
"type": "text",
"text": "Fetch the content at https://docs.langchain.com and analyze",
},
],
}
response = llm_with_tools.invoke([input_message])
assert all(isinstance(block, dict) for block in response.content)
block_types = {block["type"] for block in response.content} # type: ignore[index]
assert block_types == {"text", call_key, result_key}
# 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", call_key, result_key}
# Test we can pass back in
next_message = {
"role": "user",
"content": "What does the site you just fetched say about models?",
}
_ = llm_with_tools.invoke(
[input_message, full, next_message],
)
@pytest.mark.vcr
def test_web_fetch() -> None:
"""Note: this is a beta feature.
TODO: Update to remove beta once it's generally available.
"""
llm = ChatAnthropic(model="claude-3-5-haiku-latest", betas=["web-fetch-2025-09-10"]) # type: ignore[call-arg]
llm = ChatAnthropic(
model="claude-3-5-haiku-latest", # type: ignore[call-arg]
max_tokens=1024,
betas=["web-fetch-2025-09-10"],
)
tool = {"type": "web_fetch_20250910", "name": "web_fetch", "max_uses": 1}
llm_with_tools = llm.bind_tools([tool])
@@ -1205,7 +1456,8 @@ def test_web_fetch() -> None:
@pytest.mark.vcr
def test_code_execution() -> None:
@pytest.mark.parametrize("output_version", ["v0", "v1"])
def test_code_execution(output_version: Literal["v0", "v1"]) -> None:
"""Note: this is a beta feature.
TODO: Update to remove beta once generally available.
@@ -1214,6 +1466,7 @@ def test_code_execution() -> None:
model="claude-sonnet-4-20250514", # type: ignore[call-arg]
betas=["code-execution-2025-05-22"],
max_tokens=10_000, # type: ignore[call-arg]
output_version=output_version,
)
tool = {"type": "code_execution_20250522", "name": "code_execution"}
@@ -1234,7 +1487,10 @@ def test_code_execution() -> None:
response = llm_with_tools.invoke([input_message])
assert all(isinstance(block, dict) for block in response.content)
block_types = {block["type"] for block in response.content} # type: ignore[index]
assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}
if output_version == "v0":
assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}
else:
assert block_types == {"text", "server_tool_call", "server_tool_result"}
# Test streaming
full: Optional[BaseMessageChunk] = None
@@ -1244,7 +1500,10 @@ def test_code_execution() -> None:
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"}
if output_version == "v0":
assert block_types == {"text", "server_tool_use", "code_execution_tool_result"}
else:
assert block_types == {"text", "server_tool_call", "server_tool_result"}
# Test we can pass back in
next_message = {
@@ -1256,8 +1515,10 @@ def test_code_execution() -> None:
)
@pytest.mark.default_cassette("test_remote_mcp.yaml.gz")
@pytest.mark.vcr
def test_remote_mcp() -> None:
@pytest.mark.parametrize("output_version", ["v0", "v1"])
def test_remote_mcp(output_version: Literal["v0", "v1"]) -> None:
"""Note: this is a beta feature.
TODO: Update to remove beta once generally available.
@@ -1277,6 +1538,7 @@ def test_remote_mcp() -> None:
betas=["mcp-client-2025-04-04"],
mcp_servers=mcp_servers,
max_tokens=10_000, # type: ignore[call-arg]
output_version=output_version,
)
input_message = {
@@ -1294,7 +1556,10 @@ def test_remote_mcp() -> None:
response = llm.invoke([input_message])
assert all(isinstance(block, dict) for block in response.content)
block_types = {block["type"] for block in response.content} # type: ignore[index]
assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"}
if output_version == "v0":
assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"}
else:
assert block_types == {"text", "server_tool_call", "server_tool_result"}
# Test streaming
full: Optional[BaseMessageChunk] = None
@@ -1305,7 +1570,10 @@ def test_remote_mcp() -> None:
assert isinstance(full.content, list)
assert all(isinstance(block, dict) for block in full.content)
block_types = {block["type"] for block in full.content} # type: ignore[index]
assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"}
if output_version == "v0":
assert block_types == {"text", "mcp_tool_use", "mcp_tool_result"}
else:
assert block_types == {"text", "server_tool_call", "server_tool_result"}
# Test we can pass back in
next_message = {
@@ -1342,8 +1610,7 @@ def test_files_api_image(block_format: str) -> None:
# standard block format
block = {
"type": "image",
"source_type": "id",
"id": image_file_id,
"file_id": image_file_id,
}
input_message = {
"role": "user",
@@ -1374,8 +1641,7 @@ def test_files_api_pdf(block_format: str) -> None:
# standard block format
block = {
"type": "file",
"source_type": "id",
"id": pdf_file_id,
"file_id": pdf_file_id,
}
input_message = {
"role": "user",
@@ -1439,6 +1705,11 @@ def test_search_result_tool_message() -> None:
assert isinstance(result.content, list)
assert any("citations" in block for block in result.content)
assert (
_convert_from_v1_to_anthropic(result.content_blocks, [], "anthropic")
== result.content
)
def test_search_result_top_level() -> None:
llm = ChatAnthropic(
@@ -1484,6 +1755,11 @@ def test_search_result_top_level() -> None:
assert isinstance(result.content, list)
assert any("citations" in block for block in result.content)
assert (
_convert_from_v1_to_anthropic(result.content_blocks, [], "anthropic")
== result.content
)
def test_memory_tool() -> None:
llm = ChatAnthropic(
@@ -1512,6 +1788,7 @@ def test_context_management() -> None:
}
]
},
max_tokens=1024, # type: ignore[call-arg]
)
llm_with_tools = llm.bind_tools(
[{"type": "web_search_20250305", "name": "web_search"}]

View File

@@ -1,174 +0,0 @@
"""Test ChatAnthropic chat model."""
from __future__ import annotations
from typing import Literal, Optional
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, Field
from langchain_anthropic.experimental import ChatAnthropicTools
MODEL_NAME = "claude-3-5-haiku-latest"
BIG_MODEL_NAME = "claude-opus-4-20250514"
#####################################
### Test Basic features, no tools ###
#####################################
def test_stream() -> None:
"""Test streaming tokens from Anthropic."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
for token in llm.stream("I'm Pickle Rick"):
assert isinstance(token.content, str)
async def test_astream() -> None:
"""Test streaming tokens from Anthropic."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
async for token in llm.astream("I'm Pickle Rick"):
assert isinstance(token.content, str)
async def test_abatch() -> None:
"""Test streaming tokens from ChatAnthropicTools."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = await llm.abatch(["I'm Pickle Rick", "I'm not Pickle Rick"])
for token in result:
assert isinstance(token.content, str)
async def test_abatch_tags() -> None:
"""Test batch tokens from ChatAnthropicTools."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = await llm.abatch(
["I'm Pickle Rick", "I'm not Pickle Rick"],
config={"tags": ["foo"]},
)
for token in result:
assert isinstance(token.content, str)
def test_batch() -> None:
"""Test batch tokens from ChatAnthropicTools."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = llm.batch(["I'm Pickle Rick", "I'm not Pickle Rick"])
for token in result:
assert isinstance(token.content, str)
async def test_ainvoke() -> None:
"""Test invoke tokens from ChatAnthropicTools."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = await llm.ainvoke("I'm Pickle Rick", config={"tags": ["foo"]})
assert isinstance(result.content, str)
def test_invoke() -> None:
"""Test invoke tokens from ChatAnthropicTools."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]})
assert isinstance(result.content, str)
def test_system_invoke() -> None:
"""Test invoke tokens with a system message."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"You are an expert cartographer. If asked, you are a cartographer. "
"STAY IN CHARACTER",
),
("human", "Are you a mathematician?"),
],
)
chain = prompt | llm
result = chain.invoke({})
assert isinstance(result.content, str)
##################
### Test Tools ###
##################
def test_with_structured_output() -> None:
class Person(BaseModel):
name: str
age: int
chain = ChatAnthropicTools( # type: ignore[call-arg, call-arg]
model_name=BIG_MODEL_NAME,
temperature=0,
default_headers={"anthropic-beta": "tools-2024-04-04"},
).with_structured_output(Person)
result = chain.invoke("Erick is 27 years old")
assert isinstance(result, Person)
assert result.name == "Erick"
assert result.age == 27
def test_anthropic_complex_structured_output() -> None:
class Email(BaseModel):
"""Relevant information about an email."""
sender: Optional[str] = Field(
None,
description="The sender's name, if available",
)
sender_phone_number: Optional[str] = Field(
None,
description="The sender's phone number, if available",
)
sender_address: Optional[str] = Field(
None,
description="The sender's address, if available",
)
action_items: list[str] = Field(
...,
description="A list of action items requested by the email",
)
topic: str = Field(
...,
description="High level description of what the email is about",
)
tone: Literal["positive", "negative"] = Field(
..., description="The tone of the email."
)
prompt = ChatPromptTemplate.from_messages(
[
(
"human",
"What can you tell me about the following email? Make sure to answer in the correct format: {email}", # noqa: E501
),
],
)
llm = ChatAnthropicTools( # type: ignore[call-arg, call-arg]
temperature=0,
model_name=BIG_MODEL_NAME,
default_headers={"anthropic-beta": "tools-2024-04-04"},
)
extraction_chain = prompt | llm.with_structured_output(Email)
response = extraction_chain.invoke(
{
"email": "From: Erick. The email is about the new project. The tone is positive. The action items are to send the report and to schedule a meeting.", # noqa: E501
},
)
assert isinstance(response, Email)

View File

@@ -6,7 +6,7 @@ import pytest
from langchain_core.callbacks import CallbackManager
from langchain_core.outputs import LLMResult
from langchain_anthropic import Anthropic
from langchain_anthropic import AnthropicLLM
from tests.unit_tests._utils import FakeCallbackHandler
MODEL = "claude-3-7-sonnet-latest"
@@ -14,26 +14,26 @@ MODEL = "claude-3-7-sonnet-latest"
@pytest.mark.requires("anthropic")
def test_anthropic_model_name_param() -> None:
llm = Anthropic(model_name="foo")
llm = AnthropicLLM(model_name="foo")
assert llm.model == "foo"
@pytest.mark.requires("anthropic")
def test_anthropic_model_param() -> None:
llm = Anthropic(model="foo") # type: ignore[call-arg]
llm = AnthropicLLM(model="foo") # type: ignore[call-arg]
assert llm.model == "foo"
def test_anthropic_call() -> None:
"""Test valid call to anthropic."""
llm = Anthropic(model=MODEL) # type: ignore[call-arg]
llm = AnthropicLLM(model=MODEL) # type: ignore[call-arg]
output = llm.invoke("Say foo:")
assert isinstance(output, str)
def test_anthropic_streaming() -> None:
"""Test streaming tokens from anthropic."""
llm = Anthropic(model=MODEL) # type: ignore[call-arg]
llm = AnthropicLLM(model=MODEL) # type: ignore[call-arg]
generator = llm.stream("I'm Pickle Rick")
assert isinstance(generator, Generator)
@@ -46,7 +46,7 @@ def test_anthropic_streaming_callback() -> None:
"""Test that streaming correctly invokes on_llm_new_token callback."""
callback_handler = FakeCallbackHandler()
callback_manager = CallbackManager([callback_handler])
llm = Anthropic(
llm = AnthropicLLM(
model=MODEL, # type: ignore[call-arg]
streaming=True,
callback_manager=callback_manager,
@@ -58,7 +58,7 @@ def test_anthropic_streaming_callback() -> None:
async def test_anthropic_async_generate() -> None:
"""Test async generate."""
llm = Anthropic(model=MODEL) # type: ignore[call-arg]
llm = AnthropicLLM(model=MODEL) # type: ignore[call-arg]
output = await llm.agenerate(["How many toes do dogs have?"])
assert isinstance(output, LLMResult)
@@ -67,7 +67,7 @@ async def test_anthropic_async_streaming_callback() -> None:
"""Test that streaming correctly invokes on_llm_new_token callback."""
callback_handler = FakeCallbackHandler()
callback_manager = CallbackManager([callback_handler])
llm = Anthropic(
llm = AnthropicLLM(
model=MODEL, # type: ignore[call-arg]
streaming=True,
callback_manager=callback_manager,