anthropic[patch]: ruff fixes and rules (#31899)

* bump ruff deps
* add more thorough ruff rules
* fix said rules
This commit is contained in:
Mason Daugherty 2025-07-07 18:32:27 -04:00 committed by GitHub
parent e7eac27241
commit 2a7645300c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 430 additions and 311 deletions

View File

@ -180,13 +180,13 @@ select = [
"YTT", # flake8-2020 "YTT", # flake8-2020
] ]
ignore = [ ignore = [
"D100", "D100", # Missing docstring in public module
"D101", "D101", # Missing docstring in public class
"D102", "D102", # Missing docstring in public method
"D103", "D103", # Missing docstring in public function
"D104", "D104", # Missing docstring in public package
"D105", "D105", # Missing docstring in magic method
"D107", "D107", # Missing docstring in __init__
"COM812", # Messes with the formatter "COM812", # Messes with the formatter
"ISC001", # Messes with the formatter "ISC001", # Messes with the formatter
"PERF203", # Rarely useful "PERF203", # Rarely useful

View File

@ -6,9 +6,9 @@ from langchain_anthropic.chat_models import (
from langchain_anthropic.llms import Anthropic, AnthropicLLM from langchain_anthropic.llms import Anthropic, AnthropicLLM
__all__ = [ __all__ = [
"ChatAnthropicMessages",
"ChatAnthropic",
"convert_to_anthropic_tool",
"Anthropic", "Anthropic",
"AnthropicLLM", "AnthropicLLM",
"ChatAnthropic",
"ChatAnthropicMessages",
"convert_to_anthropic_tool",
] ]

View File

@ -6,6 +6,8 @@ for each instance of ChatAnthropic.
Logic is largely replicated from anthropic._base_client. Logic is largely replicated from anthropic._base_client.
""" """
from __future__ import annotations
import asyncio import asyncio
import os import os
from functools import lru_cache from functools import lru_cache
@ -17,7 +19,7 @@ _NOT_GIVEN: Any = object()
class _SyncHttpxClientWrapper(anthropic.DefaultHttpxClient): class _SyncHttpxClientWrapper(anthropic.DefaultHttpxClient):
"""Borrowed from anthropic._base_client""" """Borrowed from anthropic._base_client."""
def __del__(self) -> None: def __del__(self) -> None:
if self.is_closed: if self.is_closed:
@ -30,7 +32,7 @@ class _SyncHttpxClientWrapper(anthropic.DefaultHttpxClient):
class _AsyncHttpxClientWrapper(anthropic.DefaultAsyncHttpxClient): class _AsyncHttpxClientWrapper(anthropic.DefaultAsyncHttpxClient):
"""Borrowed from anthropic._base_client""" """Borrowed from anthropic._base_client."""
def __del__(self) -> None: def __del__(self) -> None:
if self.is_closed: if self.is_closed:

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import copy import copy
import json import json
import re import re
@ -111,18 +113,19 @@ def _is_builtin_tool(tool: Any) -> bool:
def _format_image(url: str) -> dict: def _format_image(url: str) -> dict:
""" """Convert part["image_url"]["url"] strings (OpenAI format) to Anthropic format.
Converts part["image_url"]["url"] strings (OpenAI format)
to the correct Anthropic format:
{ {
"type": "base64", "type": "base64",
"media_type": "image/jpeg", "media_type": "image/jpeg",
"data": "/9j/4AAQSkZJRg...", "data": "/9j/4AAQSkZJRg...",
} }
Or Or
{ {
"type": "url", "type": "url",
"url": "https://example.com/image.jpg", "url": "https://example.com/image.jpg",
} }
""" """
# Base64 encoded image # Base64 encoded image
@ -146,11 +149,14 @@ def _format_image(url: str) -> dict:
"url": url, "url": url,
} }
raise ValueError( msg = (
"Malformed url parameter." "Malformed url parameter."
" Must be either an image URL (https://example.com/image.jpg)" " Must be either an image URL (https://example.com/image.jpg)"
" or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)" " or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)"
) )
raise ValueError(
msg,
)
def _merge_messages( def _merge_messages(
@ -177,8 +183,8 @@ def _merge_messages(
"content": curr.content, "content": curr.content,
"tool_use_id": curr.tool_call_id, "tool_use_id": curr.tool_call_id,
"is_error": curr.status == "error", "is_error": curr.status == "error",
} },
] ],
) )
last = merged[-1] if merged else None last = merged[-1] if merged else None
if any( if any(
@ -187,7 +193,7 @@ def _merge_messages(
): ):
if isinstance(cast(BaseMessage, last).content, str): if isinstance(cast(BaseMessage, last).content, str):
new_content: list = [ new_content: list = [
{"type": "text", "text": cast(BaseMessage, last).content} {"type": "text", "text": cast(BaseMessage, last).content},
] ]
else: else:
new_content = copy.copy(cast(list, cast(BaseMessage, last).content)) new_content = copy.copy(cast(list, cast(BaseMessage, last).content))
@ -234,10 +240,13 @@ def _format_data_content_block(block: dict) -> dict:
}, },
} }
else: else:
raise ValueError( msg = (
"Anthropic only supports 'url' and 'base64' source_type for image " "Anthropic only supports 'url' and 'base64' source_type for image "
"content blocks." "content blocks."
) )
raise ValueError(
msg,
)
elif block["type"] == "file": elif block["type"] == "file":
if block["source_type"] == "url": if block["source_type"] == "url":
@ -276,7 +285,8 @@ def _format_data_content_block(block: dict) -> dict:
} }
else: else:
raise ValueError(f"Block of type {block['type']} is not supported.") msg = f"Block of type {block['type']} is not supported."
raise ValueError(msg)
if formatted_block: if formatted_block:
for key in ["cache_control", "citations", "title", "context"]: for key in ["cache_control", "citations", "title", "context"]:
@ -292,7 +302,6 @@ def _format_messages(
messages: Sequence[BaseMessage], messages: Sequence[BaseMessage],
) -> tuple[Union[str, list[dict], None], list[dict]]: ) -> tuple[Union[str, list[dict], None], list[dict]]:
"""Format messages for anthropic.""" """Format messages for anthropic."""
""" """
[ [
{ {
@ -308,8 +317,9 @@ def _format_messages(
for i, message in enumerate(merged_messages): for i, message in enumerate(merged_messages):
if message.type == "system": if message.type == "system":
if system is not None: if system is not None:
raise ValueError("Received multiple non-consecutive system messages.") msg = "Received multiple non-consecutive system messages."
elif isinstance(message.content, list): raise ValueError(msg)
if isinstance(message.content, list):
system = [ system = [
( (
block block
@ -328,8 +338,9 @@ def _format_messages(
if not isinstance(message.content, str): if not isinstance(message.content, str):
# parse as dict # parse as dict
if not isinstance(message.content, list): if not isinstance(message.content, list):
msg = "Anthropic message content must be str or list of dicts"
raise ValueError( raise ValueError(
"Anthropic message content must be str or list of dicts" msg,
) )
# populate content # populate content
@ -339,8 +350,9 @@ def _format_messages(
content.append({"type": "text", "text": block}) content.append({"type": "text", "text": block})
elif isinstance(block, dict): elif isinstance(block, dict):
if "type" not in block: if "type" not in block:
raise ValueError("Dict content block must have a type key") msg = "Dict content block must have a type key"
elif block["type"] == "image_url": raise ValueError(msg)
if block["type"] == "image_url":
# convert format # convert format
source = _format_image(block["image_url"]["url"]) source = _format_image(block["image_url"]["url"])
content.append({"type": "image", "source": source}) content.append({"type": "image", "source": source})
@ -358,7 +370,9 @@ def _format_messages(
if tc["id"] == block["id"] if tc["id"] == block["id"]
] ]
content.extend( content.extend(
_lc_tool_calls_to_anthropic_tool_use_blocks(overlapping) _lc_tool_calls_to_anthropic_tool_use_blocks(
overlapping,
),
) )
else: else:
block.pop("text", None) block.pop("text", None)
@ -398,7 +412,7 @@ def _format_messages(
for k, v in block.items() for k, v in block.items()
if k if k
in ("type", "text", "cache_control", "citations") in ("type", "text", "cache_control", "citations")
} },
) )
elif block["type"] == "thinking": elif block["type"] == "thinking":
content.append( content.append(
@ -407,7 +421,7 @@ def _format_messages(
for k, v in block.items() for k, v in block.items()
if k if k
in ("type", "thinking", "cache_control", "signature") in ("type", "thinking", "cache_control", "signature")
} },
) )
elif block["type"] == "redacted_thinking": elif block["type"] == "redacted_thinking":
content.append( content.append(
@ -415,13 +429,13 @@ def _format_messages(
k: v k: v
for k, v in block.items() for k, v in block.items()
if k in ("type", "cache_control", "data") if k in ("type", "cache_control", "data")
} },
) )
elif block["type"] == "tool_result": elif block["type"] == "tool_result":
tool_content = _format_messages( tool_content = _format_messages(
[HumanMessage(block["content"])] [HumanMessage(block["content"])],
)[1][0]["content"] )[1][0]["content"]
content.append({**block, **{"content": tool_content}}) content.append({**block, "content": tool_content})
elif block["type"] in ( elif block["type"] in (
"code_execution_tool_result", "code_execution_tool_result",
"mcp_tool_result", "mcp_tool_result",
@ -439,15 +453,18 @@ def _format_messages(
"is_error", # for mcp_tool_result "is_error", # for mcp_tool_result
"cache_control", "cache_control",
) )
} },
) )
else: else:
content.append(block) content.append(block)
else: else:
raise ValueError( msg = (
f"Content blocks must be str or dict, instead was: " f"Content blocks must be str or dict, instead was: "
f"{type(block)}" f"{type(block)}"
) )
raise ValueError(
msg,
)
else: else:
content = message.content content = message.content
@ -468,7 +485,7 @@ def _format_messages(
tc for tc in message.tool_calls if tc["id"] not in tool_use_ids tc for tc in message.tool_calls if tc["id"] not in tool_use_ids
] ]
cast(list, content).extend( cast(list, content).extend(
_lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls) _lc_tool_calls_to_anthropic_tool_use_blocks(missing_tool_calls),
) )
formatted_messages.append({"role": role, "content": content}) formatted_messages.append({"role": role, "content": content})
@ -481,8 +498,7 @@ def _handle_anthropic_bad_request(e: anthropic.BadRequestError) -> None:
message = "Received only system message(s). " message = "Received only system message(s). "
warnings.warn(message) warnings.warn(message)
raise e raise e
else: raise
raise
class ChatAnthropic(BaseChatModel): class ChatAnthropic(BaseChatModel):
@ -635,17 +651,17 @@ class ChatAnthropic(BaseChatModel):
.. code-block:: python .. code-block:: python
[{'name': 'GetWeather', [{'name': 'GetWeather',
'args': {'location': 'Los Angeles, CA'}, 'args': {'location': 'Los Angeles, CA'},
'id': 'toolu_01KzpPEAgzura7hpBqwHbWdo'}, 'id': 'toolu_01KzpPEAgzura7hpBqwHbWdo'},
{'name': 'GetWeather', {'name': 'GetWeather',
'args': {'location': 'New York, NY'}, 'args': {'location': 'New York, NY'},
'id': 'toolu_01JtgbVGVJbiSwtZk3Uycezx'}, 'id': 'toolu_01JtgbVGVJbiSwtZk3Uycezx'},
{'name': 'GetPopulation', {'name': 'GetPopulation',
'args': {'location': 'Los Angeles, CA'}, 'args': {'location': 'Los Angeles, CA'},
'id': 'toolu_01429aygngesudV9nTbCKGuw'}, 'id': 'toolu_01429aygngesudV9nTbCKGuw'},
{'name': 'GetPopulation', {'name': 'GetPopulation',
'args': {'location': 'New York, NY'}, 'args': {'location': 'New York, NY'},
'id': 'toolu_01JPktyd44tVMeBcPPnFSEJG'}] 'id': 'toolu_01JPktyd44tVMeBcPPnFSEJG'}]
See ``ChatAnthropic.bind_tools()`` method for more. See ``ChatAnthropic.bind_tools()`` method for more.
@ -673,7 +689,7 @@ class ChatAnthropic(BaseChatModel):
See ``ChatAnthropic.with_structured_output()`` for more. See ``ChatAnthropic.with_structured_output()`` for more.
Image input: Image input:
See `multimodal guides <https://python.langchain.com/docs/how_to/multimodal_inputs/>`_ See `multimodal guides <https://python.langchain.com/docs/how_to/multimodal_inputs/>`__
for more detail. for more detail.
.. code-block:: python .. code-block:: python
@ -717,7 +733,7 @@ class ChatAnthropic(BaseChatModel):
.. dropdown:: Files API .. dropdown:: Files API
You can also pass in files that are managed through Anthropic's You can also pass in files that are managed through Anthropic's
`Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`_: `Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`__:
.. code-block:: python .. code-block:: python
@ -744,7 +760,7 @@ class ChatAnthropic(BaseChatModel):
llm.invoke([input_message]) llm.invoke([input_message])
PDF input: PDF input:
See `multimodal guides <https://python.langchain.com/docs/how_to/multimodal_inputs/>`_ See `multimodal guides <https://python.langchain.com/docs/how_to/multimodal_inputs/>`__
for more detail. for more detail.
.. code-block:: python .. code-block:: python
@ -782,7 +798,7 @@ class ChatAnthropic(BaseChatModel):
.. dropdown:: Files API .. dropdown:: Files API
You can also pass in files that are managed through Anthropic's You can also pass in files that are managed through Anthropic's
`Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`_: `Files API <https://docs.anthropic.com/en/docs/build-with-claude/files>`__:
.. code-block:: python .. code-block:: python
@ -810,7 +826,7 @@ class ChatAnthropic(BaseChatModel):
Extended thinking: Extended thinking:
Claude 3.7 Sonnet supports an Claude 3.7 Sonnet supports an
`extended thinking <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking>`_ `extended thinking <https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking>`__
feature, which will output the step-by-step reasoning process that led to its feature, which will output the step-by-step reasoning process that led to its
final answer. final answer.
@ -838,10 +854,10 @@ class ChatAnthropic(BaseChatModel):
Citations: Citations:
Anthropic supports a Anthropic supports a
`citations <https://docs.anthropic.com/en/docs/build-with-claude/citations>`_ `citations <https://docs.anthropic.com/en/docs/build-with-claude/citations>`__
feature that lets Claude attach context to its answers based on source feature that lets Claude attach context to its answers based on source
documents supplied by the user. When documents supplied by the user. When
`document content blocks <https://docs.anthropic.com/en/docs/build-with-claude/citations#document-types>`_ `document content blocks <https://docs.anthropic.com/en/docs/build-with-claude/citations#document-types>`__
with ``"citations": {"enabled": True}`` are included in a query, Claude may with ``"citations": {"enabled": True}`` are included in a query, Claude may
generate citations in its response. generate citations in its response.
@ -924,7 +940,7 @@ class ChatAnthropic(BaseChatModel):
or by setting ``stream_usage=False`` when initializing ChatAnthropic. or by setting ``stream_usage=False`` when initializing ChatAnthropic.
Prompt caching: Prompt caching:
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/#built-in-tools>`_ See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/#built-in-tools>`__
for more detail. for more detail.
.. code-block:: python .. code-block:: python
@ -1000,11 +1016,11 @@ class ChatAnthropic(BaseChatModel):
} }
} }
See `Claude documentation <https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration-beta>`_ See `Claude documentation <https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#1-hour-cache-duration-beta>`__
for detail. for detail.
Token-efficient tool use (beta): Token-efficient tool use (beta):
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`_ See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`__
for more detail. for more detail.
.. code-block:: python .. code-block:: python
@ -1041,7 +1057,7 @@ class ChatAnthropic(BaseChatModel):
Total tokens: 408 Total tokens: 408
Built-in tools: Built-in tools:
See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`_ See LangChain `docs <https://python.langchain.com/docs/integrations/chat/anthropic/>`__
for more detail. for more detail.
.. dropdown:: Web search .. dropdown:: Web search
@ -1266,7 +1282,9 @@ class ChatAnthropic(BaseChatModel):
} }
def _get_ls_params( def _get_ls_params(
self, stop: Optional[list[str]] = None, **kwargs: Any self,
stop: Optional[list[str]] = None,
**kwargs: Any,
) -> LangSmithParams: ) -> LangSmithParams:
"""Get standard params for tracing.""" """Get standard params for tracing."""
params = self._get_invocation_params(stop=stop, **kwargs) params = self._get_invocation_params(stop=stop, **kwargs)
@ -1286,8 +1304,7 @@ class ChatAnthropic(BaseChatModel):
@classmethod @classmethod
def build_extra(cls, values: dict) -> Any: def build_extra(cls, values: dict) -> Any:
all_required_field_names = get_pydantic_field_names(cls) all_required_field_names = get_pydantic_field_names(cls)
values = _build_model_kwargs(values, all_required_field_names) return _build_model_kwargs(values, all_required_field_names)
return values
@cached_property @cached_property
def _client_params(self) -> dict[str, Any]: def _client_params(self) -> dict[str, Any]:
@ -1361,14 +1378,12 @@ class ChatAnthropic(BaseChatModel):
def _create(self, payload: dict) -> Any: def _create(self, payload: dict) -> Any:
if "betas" in payload: if "betas" in payload:
return self._client.beta.messages.create(**payload) return self._client.beta.messages.create(**payload)
else: return self._client.messages.create(**payload)
return self._client.messages.create(**payload)
async def _acreate(self, payload: dict) -> Any: async def _acreate(self, payload: dict) -> Any:
if "betas" in payload: if "betas" in payload:
return await self._async_client.beta.messages.create(**payload) return await self._async_client.beta.messages.create(**payload)
else: return await self._async_client.messages.create(**payload)
return await self._async_client.messages.create(**payload)
def _stream( def _stream(
self, self,
@ -1496,7 +1511,10 @@ class ChatAnthropic(BaseChatModel):
) -> ChatResult: ) -> ChatResult:
if self.streaming: if self.streaming:
stream_iter = self._stream( stream_iter = self._stream(
messages, stop=stop, run_manager=run_manager, **kwargs messages,
stop=stop,
run_manager=run_manager,
**kwargs,
) )
return generate_from_stream(stream_iter) return generate_from_stream(stream_iter)
payload = self._get_request_payload(messages, stop=stop, **kwargs) payload = self._get_request_payload(messages, stop=stop, **kwargs)
@ -1515,7 +1533,10 @@ class ChatAnthropic(BaseChatModel):
) -> ChatResult: ) -> ChatResult:
if self.streaming: if self.streaming:
stream_iter = self._astream( stream_iter = self._astream(
messages, stop=stop, run_manager=run_manager, **kwargs messages,
stop=stop,
run_manager=run_manager,
**kwargs,
) )
return await agenerate_from_stream(stream_iter) return await agenerate_from_stream(stream_iter)
payload = self._get_request_payload(messages, stop=stop, **kwargs) payload = self._get_request_payload(messages, stop=stop, **kwargs)
@ -1558,7 +1579,7 @@ class ChatAnthropic(BaseChatModel):
tools: Sequence[Union[dict[str, Any], type, Callable, BaseTool]], tools: Sequence[Union[dict[str, Any], type, Callable, BaseTool]],
*, *,
tool_choice: Optional[ tool_choice: Optional[
Union[dict[str, str], Literal["any", "auto"], str] Union[dict[str, str], Literal["any", "auto"], str] # noqa: PYI051
] = None, ] = None,
parallel_tool_calls: Optional[bool] = None, parallel_tool_calls: Optional[bool] = None,
**kwargs: Any, **kwargs: Any,
@ -1716,10 +1737,13 @@ class ChatAnthropic(BaseChatModel):
elif isinstance(tool_choice, str): elif isinstance(tool_choice, str):
kwargs["tool_choice"] = {"type": "tool", "name": tool_choice} kwargs["tool_choice"] = {"type": "tool", "name": tool_choice}
else: else:
raise ValueError( msg = (
f"Unrecognized 'tool_choice' type {tool_choice=}. Expected dict, " f"Unrecognized 'tool_choice' type {tool_choice=}. Expected dict, "
f"str, or None." f"str, or None."
) )
raise ValueError(
msg,
)
if parallel_tool_calls is not None: if parallel_tool_calls is not None:
disable_parallel_tool_use = not parallel_tool_calls disable_parallel_tool_use = not parallel_tool_calls
@ -1861,7 +1885,8 @@ class ChatAnthropic(BaseChatModel):
tool_name = formatted_tool["name"] tool_name = formatted_tool["name"]
if self.thinking is not None and self.thinking.get("type") == "enabled": if self.thinking is not None and self.thinking.get("type") == "enabled":
llm = self._get_llm_for_structured_output_when_thinking_is_enabled( llm = self._get_llm_for_structured_output_when_thinking_is_enabled(
schema, formatted_tool schema,
formatted_tool,
) )
else: else:
llm = self.bind_tools( llm = self.bind_tools(
@ -1875,24 +1900,27 @@ class ChatAnthropic(BaseChatModel):
if isinstance(schema, type) and is_basemodel_subclass(schema): if isinstance(schema, type) and is_basemodel_subclass(schema):
output_parser: OutputParserLike = PydanticToolsParser( output_parser: OutputParserLike = PydanticToolsParser(
tools=[schema], first_tool_only=True tools=[schema],
first_tool_only=True,
) )
else: else:
output_parser = JsonOutputKeyToolsParser( output_parser = JsonOutputKeyToolsParser(
key_name=tool_name, first_tool_only=True key_name=tool_name,
first_tool_only=True,
) )
if include_raw: if include_raw:
parser_assign = RunnablePassthrough.assign( parser_assign = RunnablePassthrough.assign(
parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None parsed=itemgetter("raw") | output_parser,
parsing_error=lambda _: None,
) )
parser_none = RunnablePassthrough.assign(parsed=lambda _: None) parser_none = RunnablePassthrough.assign(parsed=lambda _: None)
parser_with_fallback = parser_assign.with_fallbacks( parser_with_fallback = parser_assign.with_fallbacks(
[parser_none], exception_key="parsing_error" [parser_none],
exception_key="parsing_error",
) )
return RunnableMap(raw=llm) | parser_with_fallback return RunnableMap(raw=llm) | parser_with_fallback
else: return llm | output_parser
return llm | output_parser
@beta() @beta()
def get_num_tokens_from_messages( def get_num_tokens_from_messages(
@ -1909,6 +1937,8 @@ class ChatAnthropic(BaseChatModel):
messages: The message inputs to tokenize. messages: The message inputs to tokenize.
tools: If provided, sequence of dict, BaseModel, function, or BaseTools tools: If provided, sequence of dict, BaseModel, function, or BaseTools
to be converted to tool schemas. to be converted to tool schemas.
kwargs: Additional keyword arguments are passed to the
:meth:`~langchain_anthropic.chat_models.ChatAnthropic.bind` method.
Basic usage: Basic usage:
@ -1985,7 +2015,7 @@ def convert_to_anthropic_tool(
if isinstance(tool, dict) and all( if isinstance(tool, dict) and all(
k in tool for k in ("name", "description", "input_schema") k in tool for k in ("name", "description", "input_schema")
): ):
anthropic_formatted = AnthropicTool(tool) # type: ignore anthropic_formatted = AnthropicTool(tool) # type: ignore[misc]
else: else:
oai_formatted = convert_to_openai_tool(tool)["function"] oai_formatted = convert_to_openai_tool(tool)["function"]
anthropic_formatted = AnthropicTool( anthropic_formatted = AnthropicTool(
@ -2032,17 +2062,15 @@ class _AnthropicToolUse(TypedDict):
def _lc_tool_calls_to_anthropic_tool_use_blocks( def _lc_tool_calls_to_anthropic_tool_use_blocks(
tool_calls: list[ToolCall], tool_calls: list[ToolCall],
) -> list[_AnthropicToolUse]: ) -> list[_AnthropicToolUse]:
blocks = [] return [
for tool_call in tool_calls: _AnthropicToolUse(
blocks.append( type="tool_use",
_AnthropicToolUse( name=tool_call["name"],
type="tool_use", input=tool_call["args"],
name=tool_call["name"], id=cast(str, tool_call["id"]),
input=tool_call["args"],
id=cast(str, tool_call["id"]),
)
) )
return blocks for tool_call in tool_calls
]
def _make_message_chunk_from_anthropic_event( def _make_message_chunk_from_anthropic_event(
@ -2107,7 +2135,7 @@ def _make_message_chunk_from_anthropic_event(
tool_call_chunks = [] tool_call_chunks = []
message_chunk = AIMessageChunk( message_chunk = AIMessageChunk(
content=[content_block], content=[content_block],
tool_call_chunks=tool_call_chunks, # type: ignore tool_call_chunks=tool_call_chunks,
) )
block_start_event = event block_start_event = event
elif event.type == "content_block_delta": elif event.type == "content_block_delta":
@ -2122,14 +2150,10 @@ def _make_message_chunk_from_anthropic_event(
if "citation" in content_block: if "citation" in content_block:
content_block["citations"] = [content_block.pop("citation")] content_block["citations"] = [content_block.pop("citation")]
message_chunk = AIMessageChunk(content=[content_block]) message_chunk = AIMessageChunk(content=[content_block])
elif event.delta.type == "thinking_delta": elif (
content_block = event.delta.model_dump() event.delta.type == "thinking_delta"
if "text" in content_block and content_block["text"] is None: or event.delta.type == "signature_delta"
content_block.pop("text") ):
content_block["index"] = event.index
content_block["type"] = "thinking"
message_chunk = AIMessageChunk(content=[content_block])
elif event.delta.type == "signature_delta":
content_block = event.delta.model_dump() content_block = event.delta.model_dump()
if "text" in content_block and content_block["text"] is None: if "text" in content_block and content_block["text"] is None:
content_block.pop("text") content_block.pop("text")
@ -2155,7 +2179,7 @@ def _make_message_chunk_from_anthropic_event(
tool_call_chunks = [] tool_call_chunks = []
message_chunk = AIMessageChunk( message_chunk = AIMessageChunk(
content=[content_block], content=[content_block],
tool_call_chunks=tool_call_chunks, # type: ignore tool_call_chunks=tool_call_chunks,
) )
elif event.type == "message_delta" and stream_usage: elif event.type == "message_delta" and stream_usage:
usage_metadata = UsageMetadata( usage_metadata = UsageMetadata(

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import json import json
from typing import ( from typing import (
Any, Any,
@ -66,7 +68,7 @@ def get_system_message(tools: list[dict]) -> str:
parameter_description=parameter.get("description"), parameter_description=parameter.get("description"),
) )
for name, parameter in tool["parameters"]["properties"].items() for name, parameter in tool["parameters"]["properties"].items()
] ],
), ),
} }
for tool in tools for tool in tools
@ -79,7 +81,7 @@ def get_system_message(tools: list[dict]) -> str:
formatted_parameters=tool["formatted_parameters"], formatted_parameters=tool["formatted_parameters"],
) )
for tool in tools_data for tool in tools_data
] ],
) )
return SYSTEM_PROMPT_FORMAT.format(formatted_tools=tools_formatted) return SYSTEM_PROMPT_FORMAT.format(formatted_tools=tools_formatted)
@ -111,18 +113,20 @@ def _xml_to_function_call(invoke: Any, tools: list[dict]) -> dict[str, Any]:
if len(filtered_tools) > 0 and not isinstance(arguments, str): if len(filtered_tools) > 0 and not isinstance(arguments, str):
tool = filtered_tools[0] tool = filtered_tools[0]
for key, value in arguments.items(): for key, value in arguments.items():
if key in tool["parameters"]["properties"]: if (
if "type" in tool["parameters"]["properties"][key]: key in tool["parameters"]["properties"]
if tool["parameters"]["properties"][key][ and "type" in tool["parameters"]["properties"][key]
"type" ):
] == "array" and not isinstance(value, list): if tool["parameters"]["properties"][key][
arguments[key] = [value] "type"
if ( ] == "array" and not isinstance(value, list):
tool["parameters"]["properties"][key]["type"] != "object" arguments[key] = [value]
and isinstance(value, dict) if (
and len(value.keys()) == 1 tool["parameters"]["properties"][key]["type"] != "object"
): and isinstance(value, dict)
arguments[key] = list(value.values())[0] and len(value.keys()) == 1
):
arguments[key] = next(iter(value.values()))
return { return {
"function": { "function": {
@ -134,9 +138,7 @@ def _xml_to_function_call(invoke: Any, tools: list[dict]) -> dict[str, Any]:
def _xml_to_tool_calls(elem: Any, tools: list[dict]) -> list[dict[str, Any]]: def _xml_to_tool_calls(elem: Any, tools: list[dict]) -> list[dict[str, Any]]:
""" """Convert an XML element and its children into a dictionary of dictionaries."""
Convert an XML element and its children into a dictionary of dictionaries.
"""
invokes = elem.findall("invoke") invokes = elem.findall("invoke")
return [_xml_to_function_call(invoke, tools) for invoke in invokes] return [_xml_to_function_call(invoke, tools) for invoke in invokes]

View File

@ -1,3 +1,5 @@
from __future__ import annotations
import re import re
import warnings import warnings
from collections.abc import AsyncIterator, Iterator, Mapping from collections.abc import AsyncIterator, Iterator, Mapping
@ -85,8 +87,7 @@ class _AnthropicCommon(BaseLanguageModel):
@classmethod @classmethod
def build_extra(cls, values: dict) -> Any: def build_extra(cls, values: dict) -> Any:
all_required_field_names = get_pydantic_field_names(cls) all_required_field_names = get_pydantic_field_names(cls)
values = _build_model_kwargs(values, all_required_field_names) return _build_model_kwargs(values, all_required_field_names)
return values
@model_validator(mode="after") @model_validator(mode="after")
def validate_environment(self) -> Self: def validate_environment(self) -> Self:
@ -125,11 +126,12 @@ class _AnthropicCommon(BaseLanguageModel):
@property @property
def _identifying_params(self) -> Mapping[str, Any]: def _identifying_params(self) -> Mapping[str, Any]:
"""Get the identifying parameters.""" """Get the identifying parameters."""
return {**{}, **self._default_params} return {**self._default_params}
def _get_anthropic_stop(self, stop: Optional[list[str]] = None) -> list[str]: def _get_anthropic_stop(self, stop: Optional[list[str]] = None) -> list[str]:
if not self.HUMAN_PROMPT or not self.AI_PROMPT: if not self.HUMAN_PROMPT or not self.AI_PROMPT:
raise NameError("Please ensure the anthropic package is loaded") msg = "Please ensure the anthropic package is loaded"
raise NameError(msg)
if stop is None: if stop is None:
stop = [] stop = []
@ -152,6 +154,7 @@ class AnthropicLLM(LLM, _AnthropicCommon):
from langchain_anthropic import AnthropicLLM from langchain_anthropic import AnthropicLLM
model = AnthropicLLM() model = AnthropicLLM()
""" """
model_config = ConfigDict( model_config = ConfigDict(
@ -166,7 +169,7 @@ class AnthropicLLM(LLM, _AnthropicCommon):
warnings.warn( warnings.warn(
"This Anthropic LLM is deprecated. " "This Anthropic LLM is deprecated. "
"Please use `from langchain_anthropic import ChatAnthropic` " "Please use `from langchain_anthropic import ChatAnthropic` "
"instead" "instead",
) )
return values return values
@ -199,7 +202,9 @@ class AnthropicLLM(LLM, _AnthropicCommon):
} }
def _get_ls_params( def _get_ls_params(
self, stop: Optional[list[str]] = None, **kwargs: Any self,
stop: Optional[list[str]] = None,
**kwargs: Any,
) -> LangSmithParams: ) -> LangSmithParams:
"""Get standard params for tracing.""" """Get standard params for tracing."""
params = super()._get_ls_params(stop=stop, **kwargs) params = super()._get_ls_params(stop=stop, **kwargs)
@ -213,7 +218,8 @@ class AnthropicLLM(LLM, _AnthropicCommon):
def _wrap_prompt(self, prompt: str) -> str: def _wrap_prompt(self, prompt: str) -> str:
if not self.HUMAN_PROMPT or not self.AI_PROMPT: if not self.HUMAN_PROMPT or not self.AI_PROMPT:
raise NameError("Please ensure the anthropic package is loaded") msg = "Please ensure the anthropic package is loaded"
raise NameError(msg)
if prompt.startswith(self.HUMAN_PROMPT): if prompt.startswith(self.HUMAN_PROMPT):
return prompt # Already wrapped. return prompt # Already wrapped.
@ -238,6 +244,8 @@ class AnthropicLLM(LLM, _AnthropicCommon):
Args: Args:
prompt: The prompt to pass into the model. prompt: The prompt to pass into the model.
stop: Optional list of stop words to use when generating. stop: Optional list of stop words to use when generating.
run_manager: Optional callback manager for LLM run.
kwargs: Additional keyword arguments to pass to the model.
Returns: Returns:
The string generated by the model. The string generated by the model.
@ -253,7 +261,10 @@ class AnthropicLLM(LLM, _AnthropicCommon):
if self.streaming: if self.streaming:
completion = "" completion = ""
for chunk in self._stream( for chunk in self._stream(
prompt=prompt, stop=stop, run_manager=run_manager, **kwargs prompt=prompt,
stop=stop,
run_manager=run_manager,
**kwargs,
): ):
completion += chunk.text completion += chunk.text
return completion return completion
@ -281,7 +292,10 @@ class AnthropicLLM(LLM, _AnthropicCommon):
if self.streaming: if self.streaming:
completion = "" completion = ""
async for chunk in self._astream( async for chunk in self._astream(
prompt=prompt, stop=stop, run_manager=run_manager, **kwargs prompt=prompt,
stop=stop,
run_manager=run_manager,
**kwargs,
): ):
completion += chunk.text completion += chunk.text
return completion return completion
@ -308,8 +322,12 @@ class AnthropicLLM(LLM, _AnthropicCommon):
Args: Args:
prompt: The prompt to pass into the model. prompt: The prompt to pass into the model.
stop: Optional list of stop words to use when generating. stop: Optional list of stop words to use when generating.
run_manager: Optional callback manager for LLM run.
kwargs: Additional keyword arguments to pass to the model.
Returns: Returns:
A generator representing the stream of tokens from Anthropic. A generator representing the stream of tokens from Anthropic.
Example: Example:
.. code-block:: python .. code-block:: python
@ -319,12 +337,16 @@ class AnthropicLLM(LLM, _AnthropicCommon):
generator = anthropic.stream(prompt) generator = anthropic.stream(prompt)
for token in generator: for token in generator:
yield token yield token
""" """
stop = self._get_anthropic_stop(stop) stop = self._get_anthropic_stop(stop)
params = {**self._default_params, **kwargs} params = {**self._default_params, **kwargs}
for token in self.client.completions.create( for token in self.client.completions.create(
prompt=self._wrap_prompt(prompt), stop_sequences=stop, stream=True, **params prompt=self._wrap_prompt(prompt),
stop_sequences=stop,
stream=True,
**params,
): ):
chunk = GenerationChunk(text=token.completion) chunk = GenerationChunk(text=token.completion)
@ -344,8 +366,12 @@ class AnthropicLLM(LLM, _AnthropicCommon):
Args: Args:
prompt: The prompt to pass into the model. prompt: The prompt to pass into the model.
stop: Optional list of stop words to use when generating. stop: Optional list of stop words to use when generating.
run_manager: Optional callback manager for LLM run.
kwargs: Additional keyword arguments to pass to the model.
Returns: Returns:
A generator representing the stream of tokens from Anthropic. A generator representing the stream of tokens from Anthropic.
Example: Example:
.. code-block:: python .. code-block:: python
@ -354,6 +380,7 @@ class AnthropicLLM(LLM, _AnthropicCommon):
generator = anthropic.stream(prompt) generator = anthropic.stream(prompt)
for token in generator: for token in generator:
yield token yield token
""" """
stop = self._get_anthropic_stop(stop) stop = self._get_anthropic_stop(stop)
params = {**self._default_params, **kwargs} params = {**self._default_params, **kwargs}
@ -372,15 +399,16 @@ class AnthropicLLM(LLM, _AnthropicCommon):
def get_num_tokens(self, text: str) -> int: def get_num_tokens(self, text: str) -> int:
"""Calculate number of tokens.""" """Calculate number of tokens."""
raise NotImplementedError( msg = (
"Anthropic's legacy count_tokens method was removed in anthropic 0.39.0 " "Anthropic's legacy count_tokens method was removed in anthropic 0.39.0 "
"and langchain-anthropic 0.3.0. Please use " "and langchain-anthropic 0.3.0. Please use "
"ChatAnthropic.get_num_tokens_from_messages instead." "ChatAnthropic.get_num_tokens_from_messages instead."
) )
raise NotImplementedError(
msg,
)
@deprecated(since="0.1.0", removal="1.0.0", alternative="AnthropicLLM") @deprecated(since="0.1.0", removal="1.0.0", alternative="AnthropicLLM")
class Anthropic(AnthropicLLM): class Anthropic(AnthropicLLM):
"""Anthropic large language model.""" """Anthropic large language model."""
pass

View File

@ -1,3 +1,5 @@
from __future__ import annotations
from typing import Any, Optional, Union, cast from typing import Any, Optional, Union, cast
from langchain_core.messages import AIMessage, ToolCall from langchain_core.messages import AIMessage, ToolCall
@ -27,8 +29,12 @@ class ToolsOutputParser(BaseGenerationOutputParser):
Args: Args:
result: A list of Generations to be parsed. The Generations are assumed result: A list of Generations to be parsed. The Generations are assumed
to be different candidate outputs for a single model input. to be different candidate outputs for a single model input.
partial: (Not used) Whether the result is a partial result. If True, the
parser may return a partial result, which may not be complete or valid.
Returns: Returns:
Structured output. Structured output.
""" """
if not result or not isinstance(result[0], ChatGeneration): if not result or not isinstance(result[0], ChatGeneration):
return None if self.first_tool_only else [] return None if self.first_tool_only else []
@ -53,8 +59,7 @@ class ToolsOutputParser(BaseGenerationOutputParser):
if self.first_tool_only: if self.first_tool_only:
return tool_calls[0] if tool_calls else None return tool_calls[0] if tool_calls else None
else: return list(tool_calls)
return [tool_call for tool_call in tool_calls]
def _pydantic_parse(self, tool_call: dict) -> BaseModel: def _pydantic_parse(self, tool_call: dict) -> BaseModel:
cls_ = {schema.__name__: schema for schema in self.pydantic_schemas or []}[ cls_ = {schema.__name__: schema for schema in self.pydantic_schemas or []}[
@ -80,8 +85,7 @@ def extract_tool_calls(content: Union[str, list[Union[str, dict]]]) -> list[Tool
if block["type"] != "tool_use": if block["type"] != "tool_use":
continue continue
tool_calls.append( tool_calls.append(
tool_call(name=block["name"], args=block["input"], id=block["id"]) tool_call(name=block["name"], args=block["input"], id=block["id"]),
) )
return tool_calls return tool_calls
else: return []
return []

View File

@ -60,8 +60,58 @@ plugins = ['pydantic.mypy']
target-version = "py39" target-version = "py39"
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "I", "T201", "UP", "S"] select = [
ignore = [ "UP007", ] "A", # flake8-builtins
"ASYNC", # flake8-async
"C4", # flake8-comprehensions
"COM", # flake8-commas
"D", # pydocstyle
"DOC", # pydoclint
"E", # pycodestyle error
"EM", # flake8-errmsg
"F", # pyflakes
"FA", # flake8-future-annotations
"FBT", # flake8-boolean-trap
"FLY", # flake8-flynt
"I", # isort
"ICN", # flake8-import-conventions
"INT", # flake8-gettext
"ISC", # isort-comprehensions
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PERF", # flake8-perf
"PYI", # flake8-pyi
"Q", # flake8-quotes
"RET", # flake8-return
"RSE", # flake8-rst-docstrings
"RUF", # ruff
"S", # flake8-bandit
"SLF", # flake8-self
"SLOT", # flake8-slots
"SIM", # flake8-simplify
"T10", # flake8-debugger
"T20", # flake8-print
"TID", # flake8-tidy-imports
"UP", # pyupgrade
"W", # pycodestyle warning
"YTT", # flake8-2020
]
ignore = [
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D102", # Missing docstring in public method
"D103", # Missing docstring in public function
"D104", # Missing docstring in public package
"D105", # Missing docstring in magic method
"D107", # Missing docstring in __init__
"D214", # Section over-indented, doesn't play well with reStructuredText
"COM812", # Messes with the formatter
"ISC001", # Messes with the formatter
"PERF203", # Rarely useful
"UP007", # non-pep604-annotation-union
"UP045", # non-pep604-annotation-optional
"SIM105", # Rarely useful
]
[tool.coverage.run] [tool.coverage.run]
omit = ["tests/*"] omit = ["tests/*"]
@ -78,4 +128,5 @@ asyncio_mode = "auto"
"tests/**/*.py" = [ "tests/**/*.py" = [
"S101", # Tests need assertions "S101", # Tests need assertions
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
"SLF001", # Private member access in tests
] ]

View File

@ -20,9 +20,7 @@ def remove_response_headers(response: dict) -> dict:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def vcr_config(_base_vcr_config: dict) -> dict: # noqa: F811 def vcr_config(_base_vcr_config: dict) -> dict: # noqa: F811
""" """Extend the default configuration coming from langchain_tests."""
Extend the default configuration coming from langchain_tests.
"""
config = _base_vcr_config.copy() config = _base_vcr_config.copy()
config["before_record_request"] = remove_request_headers config["before_record_request"] = remove_request_headers
config["before_record_response"] = remove_response_headers config["before_record_response"] = remove_response_headers

View File

@ -1,5 +1,7 @@
"""Test ChatAnthropic chat model.""" """Test ChatAnthropic chat model."""
from __future__ import annotations
import asyncio import asyncio
import json import json
import os import os
@ -43,10 +45,7 @@ def test_stream() -> None:
chunks_with_model_name = 0 chunks_with_model_name = 0
for token in llm.stream("I'm Pickle Rick"): for token in llm.stream("I'm Pickle Rick"):
assert isinstance(token.content, str) assert isinstance(token.content, str)
if full is None: full = cast(BaseMessageChunk, token) if full is None else full + token
full = cast(BaseMessageChunk, token)
else:
full = full + token
assert isinstance(token, AIMessageChunk) assert isinstance(token, AIMessageChunk)
if token.usage_metadata is not None: if token.usage_metadata is not None:
if token.usage_metadata.get("input_tokens"): if token.usage_metadata.get("input_tokens"):
@ -55,11 +54,14 @@ def test_stream() -> None:
chunks_with_output_token_counts += 1 chunks_with_output_token_counts += 1
chunks_with_model_name += int("model_name" in token.response_metadata) chunks_with_model_name += int("model_name" in token.response_metadata)
if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1: if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1:
raise AssertionError( msg = (
"Expected exactly one chunk with input or output token counts. " "Expected exactly one chunk with input or output token counts. "
"AIMessageChunk aggregation adds counts. Check that " "AIMessageChunk aggregation adds counts. Check that "
"this is behaving properly." "this is behaving properly."
) )
raise AssertionError(
msg,
)
assert chunks_with_model_name == 1 assert chunks_with_model_name == 1
# check token usage is populated # check token usage is populated
assert isinstance(full, AIMessageChunk) assert isinstance(full, AIMessageChunk)
@ -85,10 +87,7 @@ async def test_astream() -> None:
chunks_with_output_token_counts = 0 chunks_with_output_token_counts = 0
async for token in llm.astream("I'm Pickle Rick"): async for token in llm.astream("I'm Pickle Rick"):
assert isinstance(token.content, str) assert isinstance(token.content, str)
if full is None: full = cast(BaseMessageChunk, token) if full is None else full + token
full = cast(BaseMessageChunk, token)
else:
full = full + token
assert isinstance(token, AIMessageChunk) assert isinstance(token, AIMessageChunk)
if token.usage_metadata is not None: if token.usage_metadata is not None:
if token.usage_metadata.get("input_tokens"): if token.usage_metadata.get("input_tokens"):
@ -96,11 +95,14 @@ async def test_astream() -> None:
if token.usage_metadata.get("output_tokens"): if token.usage_metadata.get("output_tokens"):
chunks_with_output_token_counts += 1 chunks_with_output_token_counts += 1
if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1: if chunks_with_input_token_counts != 1 or chunks_with_output_token_counts != 1:
raise AssertionError( msg = (
"Expected exactly one chunk with input or output token counts. " "Expected exactly one chunk with input or output token counts. "
"AIMessageChunk aggregation adds counts. Check that " "AIMessageChunk aggregation adds counts. Check that "
"this is behaving properly." "this is behaving properly."
) )
raise AssertionError(
msg,
)
# check token usage is populated # check token usage is populated
assert isinstance(full, AIMessageChunk) assert isinstance(full, AIMessageChunk)
assert full.usage_metadata is not None assert full.usage_metadata is not None
@ -167,7 +169,8 @@ async def test_abatch_tags() -> None:
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = await llm.abatch( result = await llm.abatch(
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]} ["I'm Pickle Rick", "I'm not Pickle Rick"],
config={"tags": ["foo"]},
) )
for token in result: for token in result:
assert isinstance(token.content, str) assert isinstance(token.content, str)
@ -187,8 +190,8 @@ async def test_async_tool_use() -> None:
"type": "object", "type": "object",
"properties": {"location": {"type": "string"}}, "properties": {"location": {"type": "string"}},
}, },
} },
] ],
) )
response = await llm_with_tools.ainvoke("what's the weather in san francisco, ca") response = await llm_with_tools.ainvoke("what's the weather in san francisco, ca")
assert isinstance(response, AIMessage) assert isinstance(response, AIMessage)
@ -202,16 +205,16 @@ async def test_async_tool_use() -> None:
# Test streaming # Test streaming
first = True first = True
chunks = [] # type: ignore chunks: list[BaseMessage | BaseMessageChunk] = []
async for chunk in llm_with_tools.astream( async for chunk in llm_with_tools.astream(
"what's the weather in san francisco, ca" "what's the weather in san francisco, ca",
): ):
chunks = chunks + [chunk] chunks = [*chunks, chunk]
if first: if first:
gathered = chunk gathered = chunk
first = False first = False
else: else:
gathered = gathered + chunk # type: ignore gathered = gathered + chunk # type: ignore[assignment]
assert len(chunks) > 1 assert len(chunks) > 1
assert isinstance(gathered, AIMessageChunk) assert isinstance(gathered, AIMessageChunk)
assert isinstance(gathered.tool_call_chunks, list) assert isinstance(gathered.tool_call_chunks, list)
@ -244,12 +247,12 @@ def test_invoke() -> None:
"""Test invoke tokens from ChatAnthropicMessages.""" """Test invoke tokens from ChatAnthropicMessages."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"])) result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]})
assert isinstance(result.content, str) assert isinstance(result.content, str)
def test_system_invoke() -> None: def test_system_invoke() -> None:
"""Test invoke tokens with a system message""" """Test invoke tokens with a system message."""
llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] llm = ChatAnthropicMessages(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
prompt = ChatPromptTemplate.from_messages( prompt = ChatPromptTemplate.from_messages(
@ -260,7 +263,7 @@ def test_system_invoke() -> None:
"STAY IN CHARACTER", "STAY IN CHARACTER",
), ),
("human", "Are you a mathematician?"), ("human", "Are you a mathematician?"),
] ],
) )
chain = prompt | llm chain = prompt | llm
@ -282,7 +285,7 @@ def test_anthropic_generate() -> None:
"""Test generate method of anthropic.""" """Test generate method of anthropic."""
chat = ChatAnthropic(model=MODEL_NAME) chat = ChatAnthropic(model=MODEL_NAME)
chat_messages: list[list[BaseMessage]] = [ chat_messages: list[list[BaseMessage]] = [
[HumanMessage(content="How many toes do dogs have?")] [HumanMessage(content="How many toes do dogs have?")],
] ]
messages_copy = [messages.copy() for messages in chat_messages] messages_copy = [messages.copy() for messages in chat_messages]
result: LLMResult = chat.generate(chat_messages) result: LLMResult = chat.generate(chat_messages)
@ -330,7 +333,7 @@ async def test_anthropic_async_streaming_callback() -> None:
verbose=True, verbose=True,
) )
chat_messages: list[BaseMessage] = [ chat_messages: list[BaseMessage] = [
HumanMessage(content="How many toes do dogs have?") HumanMessage(content="How many toes do dogs have?"),
] ]
async for token in chat.astream(chat_messages): async for token in chat.astream(chat_messages):
assert isinstance(token, AIMessageChunk) assert isinstance(token, AIMessageChunk)
@ -352,8 +355,8 @@ def test_anthropic_multimodal() -> None:
}, },
}, },
{"type": "text", "text": "What is this a logo for?"}, {"type": "text", "text": "What is this a logo for?"},
] ],
) ),
] ]
response = chat.invoke(messages) response = chat.invoke(messages)
assert isinstance(response, AIMessage) assert isinstance(response, AIMessage)
@ -368,7 +371,9 @@ def test_streaming() -> None:
callback_manager = CallbackManager([callback_handler]) callback_manager = CallbackManager([callback_handler])
llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg] llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg]
model_name=MODEL_NAME, streaming=True, callback_manager=callback_manager model_name=MODEL_NAME,
streaming=True,
callback_manager=callback_manager,
) )
response = llm.generate([[HumanMessage(content="I'm Pickle Rick")]]) response = llm.generate([[HumanMessage(content="I'm Pickle Rick")]])
@ -382,7 +387,9 @@ async def test_astreaming() -> None:
callback_manager = CallbackManager([callback_handler]) callback_manager = CallbackManager([callback_handler])
llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg] llm = ChatAnthropicMessages( # type: ignore[call-arg, call-arg]
model_name=MODEL_NAME, streaming=True, callback_manager=callback_manager model_name=MODEL_NAME,
streaming=True,
callback_manager=callback_manager,
) )
response = await llm.agenerate([[HumanMessage(content="I'm Pickle Rick")]]) response = await llm.agenerate([[HumanMessage(content="I'm Pickle Rick")]])
@ -421,19 +428,19 @@ def test_tool_use() -> None:
temperature=0, temperature=0,
# Add extra headers to also test token-efficient tools # Add extra headers to also test token-efficient tools
model_kwargs={ model_kwargs={
"extra_headers": {"anthropic-beta": "token-efficient-tools-2025-02-19"} "extra_headers": {"anthropic-beta": "token-efficient-tools-2025-02-19"},
}, },
) )
llm_with_tools = llm.bind_tools([tool_definition]) llm_with_tools = llm.bind_tools([tool_definition])
first = True first = True
chunks = [] # type: ignore chunks: list[BaseMessage | BaseMessageChunk] = []
for chunk in llm_with_tools.stream(query): for chunk in llm_with_tools.stream(query):
chunks = chunks + [chunk] chunks = [*chunks, chunk]
if first: if first:
gathered = chunk gathered = chunk
first = False first = False
else: else:
gathered = gathered + chunk # type: ignore gathered = gathered + chunk # type: ignore[assignment]
assert len(chunks) > 1 assert len(chunks) > 1
assert isinstance(gathered.content, list) assert isinstance(gathered.content, list)
assert len(gathered.content) == 2 assert len(gathered.content) == 2
@ -470,17 +477,17 @@ def test_tool_use() -> None:
query, query,
gathered, gathered,
ToolMessage(content="sunny and warm", tool_call_id=tool_call["id"]), ToolMessage(content="sunny and warm", tool_call_id=tool_call["id"]),
] ],
) )
chunks = [] # type: ignore chunks = []
first = True first = True
for chunk in stream: for chunk in stream:
chunks = chunks + [chunk] chunks = [*chunks, chunk]
if first: if first:
gathered = chunk gathered = chunk
first = False first = False
else: else:
gathered = gathered + chunk # type: ignore gathered = gathered + chunk # type: ignore[assignment]
assert len(chunks) > 1 assert len(chunks) > 1
@ -489,14 +496,14 @@ def test_builtin_tools() -> None:
tool = {"type": "text_editor_20250124", "name": "str_replace_editor"} tool = {"type": "text_editor_20250124", "name": "str_replace_editor"}
llm_with_tools = llm.bind_tools([tool]) llm_with_tools = llm.bind_tools([tool])
response = llm_with_tools.invoke( response = llm_with_tools.invoke(
"There's a syntax error in my primes.py file. Can you help me fix it?" "There's a syntax error in my primes.py file. Can you help me fix it?",
) )
assert isinstance(response, AIMessage) assert isinstance(response, AIMessage)
assert response.tool_calls assert response.tool_calls
class GenerateUsername(BaseModel): class GenerateUsername(BaseModel):
"Get a username based on someone's name and hair color." """Get a username based on someone's name and hair color."""
name: str name: str
hair_color: str hair_color: str
@ -508,7 +515,7 @@ def test_disable_parallel_tool_calling() -> None:
result = llm_with_tools.invoke( result = llm_with_tools.invoke(
"Use the GenerateUsername tool to generate user names for:\n\n" "Use the GenerateUsername tool to generate user names for:\n\n"
"Sally with green hair\n" "Sally with green hair\n"
"Bob with blue hair" "Bob with blue hair",
) )
assert isinstance(result, AIMessage) assert isinstance(result, AIMessage)
assert len(result.tool_calls) == 1 assert len(result.tool_calls) == 1
@ -523,7 +530,7 @@ def test_anthropic_with_empty_text_block() -> None:
return "OK" return "OK"
model = ChatAnthropic(model="claude-3-opus-20240229", temperature=0).bind_tools( model = ChatAnthropic(model="claude-3-opus-20240229", temperature=0).bind_tools(
[type_letter] [type_letter],
) )
messages = [ messages = [
@ -531,7 +538,7 @@ def test_anthropic_with_empty_text_block() -> None:
content="Repeat the given string using the provided tools. Do not write " content="Repeat the given string using the provided tools. Do not write "
"anything else or provide any explanations. For example, " "anything else or provide any explanations. For example, "
"if the string is 'abc', you must print the " "if the string is 'abc', you must print the "
"letters 'a', 'b', and 'c' one at a time and in that order. " "letters 'a', 'b', and 'c' one at a time and in that order. ",
), ),
HumanMessage(content="dog"), HumanMessage(content="dog"),
AIMessage( AIMessage(
@ -572,7 +579,7 @@ def test_with_structured_output() -> None:
"type": "object", "type": "object",
"properties": {"location": {"type": "string"}}, "properties": {"location": {"type": "string"}},
}, },
} },
) )
response = structured_llm.invoke("what's the weather in san francisco, ca") response = structured_llm.invoke("what's the weather in san francisco, ca")
assert isinstance(response, dict) assert isinstance(response, dict)
@ -593,10 +600,11 @@ def test_get_num_tokens_from_messages() -> None:
# Test tool use # Test tool use
@tool(parse_docstring=True) @tool(parse_docstring=True)
def get_weather(location: str) -> str: def get_weather(location: str) -> str:
"""Get the current weather in a given location """Get the current weather in a given location.
Args: Args:
location: The city and state, e.g. San Francisco, CA location: The city and state, e.g. San Francisco, CA
""" """
return "Sunny" return "Sunny"
@ -634,7 +642,7 @@ def test_get_num_tokens_from_messages() -> None:
class GetWeather(BaseModel): class GetWeather(BaseModel):
"""Get the current weather in a given location""" """Get the current weather in a given location."""
location: str = Field(..., description="The city and state, e.g. San Francisco, CA") location: str = Field(..., description="The city and state, e.g. San Francisco, CA")
@ -666,9 +674,9 @@ def test_pdf_document_input() -> None:
"media_type": "application/pdf", "media_type": "application/pdf",
}, },
}, },
] ],
) ),
] ],
) )
assert isinstance(result, AIMessage) assert isinstance(result, AIMessage)
assert isinstance(result.content, str) assert isinstance(result.content, str)
@ -694,7 +702,7 @@ def test_citations() -> None:
}, },
{"type": "text", "text": "What color is the grass and sky?"}, {"type": "text", "text": "What color is the grass and sky?"},
], ],
} },
] ]
response = llm.invoke(messages) response = llm.invoke(messages)
assert isinstance(response, AIMessage) assert isinstance(response, AIMessage)
@ -704,10 +712,7 @@ def test_citations() -> None:
# Test streaming # Test streaming
full: Optional[BaseMessageChunk] = None full: Optional[BaseMessageChunk] = None
for chunk in llm.stream(messages): for chunk in llm.stream(messages):
if full is None: full = cast(BaseMessageChunk, chunk) if full is None else full + chunk
full = cast(BaseMessageChunk, chunk)
else:
full = full + chunk
assert isinstance(full, AIMessageChunk) assert isinstance(full, AIMessageChunk)
assert isinstance(full.content, list) assert isinstance(full.content, list)
assert any("citations" in block for block in full.content) assert any("citations" in block for block in full.content)
@ -718,7 +723,7 @@ def test_citations() -> None:
"role": "user", "role": "user",
"content": "Can you comment on the citations you just made?", "content": "Can you comment on the citations you just made?",
} }
_ = llm.invoke(messages + [full, next_message]) _ = llm.invoke([*messages, full, next_message])
@pytest.mark.vcr @pytest.mark.vcr
@ -742,10 +747,7 @@ def test_thinking() -> None:
# Test streaming # Test streaming
full: Optional[BaseMessageChunk] = None full: Optional[BaseMessageChunk] = None
for chunk in llm.stream([input_message]): for chunk in llm.stream([input_message]):
if full is None: full = cast(BaseMessageChunk, chunk) if full is None else full + chunk
full = cast(BaseMessageChunk, chunk)
else:
full = full + chunk
assert isinstance(full, AIMessageChunk) assert isinstance(full, AIMessageChunk)
assert isinstance(full.content, list) assert isinstance(full.content, list)
assert any("thinking" in block for block in full.content) assert any("thinking" in block for block in full.content)
@ -784,10 +786,7 @@ def test_redacted_thinking() -> None:
# Test streaming # Test streaming
full: Optional[BaseMessageChunk] = None full: Optional[BaseMessageChunk] = None
for chunk in llm.stream([input_message]): for chunk in llm.stream([input_message]):
if full is None: full = cast(BaseMessageChunk, chunk) if full is None else full + chunk
full = cast(BaseMessageChunk, chunk)
else:
full = full + chunk
assert isinstance(full, AIMessageChunk) assert isinstance(full, AIMessageChunk)
assert isinstance(full.content, list) assert isinstance(full.content, list)
stream_has_reasoning = False stream_has_reasoning = False
@ -864,7 +863,7 @@ def test_image_tool_calling() -> None:
"media_type": "image/jpeg", "media_type": "image/jpeg",
"data": image_data, "data": image_data,
}, },
} },
) )
messages = [ messages = [
SystemMessage("you're a good assistant"), SystemMessage("you're a good assistant"),
@ -878,7 +877,7 @@ def test_image_tool_calling() -> None:
"id": "foo", "id": "foo",
"name": "color_picker", "name": "color_picker",
}, },
] ],
), ),
HumanMessage( HumanMessage(
[ [
@ -889,12 +888,12 @@ def test_image_tool_calling() -> None:
{ {
"type": "text", "type": "text",
"text": "green is a great pick! that's my sister's favorite color", # noqa: E501 "text": "green is a great pick! that's my sister's favorite color", # noqa: E501
} },
], ],
"is_error": False, "is_error": False,
}, },
{"type": "text", "text": "what's my sister's favorite color"}, {"type": "text", "text": "what's my sister's favorite color"},
] ],
), ),
] ]
llm = ChatAnthropic(model="claude-3-5-sonnet-latest") llm = ChatAnthropic(model="claude-3-5-sonnet-latest")
@ -914,7 +913,7 @@ def test_web_search() -> None:
{ {
"type": "text", "type": "text",
"text": "How do I update a web app to TypeScript 5.5?", "text": "How do I update a web app to TypeScript 5.5?",
} },
], ],
} }
response = llm_with_tools.invoke([input_message]) response = llm_with_tools.invoke([input_message])
@ -962,7 +961,7 @@ def test_code_execution() -> None:
"Calculate the mean and standard deviation of " "Calculate the mean and standard deviation of "
"[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]"
), ),
} },
], ],
} }
response = llm_with_tools.invoke([input_message]) response = llm_with_tools.invoke([input_message])
@ -999,7 +998,7 @@ def test_remote_mcp() -> None:
"name": "deepwiki", "name": "deepwiki",
"tool_configuration": {"enabled": True, "allowed_tools": ["ask_question"]}, "tool_configuration": {"enabled": True, "allowed_tools": ["ask_question"]},
"authorization_token": "PLACEHOLDER", "authorization_token": "PLACEHOLDER",
} },
] ]
llm = ChatAnthropic( llm = ChatAnthropic(
@ -1018,7 +1017,7 @@ def test_remote_mcp() -> None:
"What transport protocols does the 2025-03-26 version of the MCP " "What transport protocols does the 2025-03-26 version of the MCP "
"spec (modelcontextprotocol/modelcontextprotocol) support?" "spec (modelcontextprotocol/modelcontextprotocol) support?"
), ),
} },
], ],
} }
response = llm.invoke([input_message]) response = llm.invoke([input_message])
@ -1132,9 +1131,9 @@ def test_search_result_tool_message() -> None:
"To request vacation days, submit a leave request form " "To request vacation days, submit a leave request form "
"through the HR portal. Approval will be sent by email." "through the HR portal. Approval will be sent by email."
), ),
} },
], ],
} },
] ]
tool_call = { tool_call = {
@ -1182,7 +1181,7 @@ def test_search_result_top_level() -> None:
"To request vacation days, submit a leave request form " "To request vacation days, submit a leave request form "
"through the HR portal. Approval will be sent by email." "through the HR portal. Approval will be sent by email."
), ),
} },
], ],
}, },
{ {
@ -1194,14 +1193,14 @@ def test_search_result_top_level() -> None:
{ {
"type": "text", "type": "text",
"text": "Managers have 3 days to approve a request.", "text": "Managers have 3 days to approve a request.",
} },
], ],
}, },
{ {
"type": "text", "type": "text",
"text": "How do I request vacation days?", "text": "How do I request vacation days?",
}, },
] ],
) )
result = llm.invoke([input_message]) result = llm.invoke([input_message])
assert isinstance(result, AIMessage) assert isinstance(result, AIMessage)

View File

@ -4,4 +4,3 @@ import pytest
@pytest.mark.compile @pytest.mark.compile
def test_placeholder() -> None: def test_placeholder() -> None:
"""Used for compiling integration tests without running any real tests.""" """Used for compiling integration tests without running any real tests."""
pass

View File

@ -1,5 +1,7 @@
"""Test ChatAnthropic chat model.""" """Test ChatAnthropic chat model."""
from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional
@ -46,7 +48,8 @@ async def test_abatch_tags() -> None:
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = await llm.abatch( result = await llm.abatch(
["I'm Pickle Rick", "I'm not Pickle Rick"], config={"tags": ["foo"]} ["I'm Pickle Rick", "I'm not Pickle Rick"],
config={"tags": ["foo"]},
) )
for token in result: for token in result:
assert isinstance(token.content, str) assert isinstance(token.content, str)
@ -73,12 +76,12 @@ def test_invoke() -> None:
"""Test invoke tokens from ChatAnthropicTools.""" """Test invoke tokens from ChatAnthropicTools."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"])) result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]})
assert isinstance(result.content, str) assert isinstance(result.content, str)
def test_system_invoke() -> None: def test_system_invoke() -> None:
"""Test invoke tokens with a system message""" """Test invoke tokens with a system message."""
llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg]
prompt = ChatPromptTemplate.from_messages( prompt = ChatPromptTemplate.from_messages(
@ -89,7 +92,7 @@ def test_system_invoke() -> None:
"STAY IN CHARACTER", "STAY IN CHARACTER",
), ),
("human", "Are you a mathematician?"), ("human", "Are you a mathematician?"),
] ],
) )
chain = prompt | llm chain = prompt | llm
@ -128,19 +131,24 @@ def test_anthropic_complex_structured_output() -> None:
"""Relevant information about an email.""" """Relevant information about an email."""
sender: Optional[str] = Field( sender: Optional[str] = Field(
None, description="The sender's name, if available" None,
description="The sender's name, if available",
) )
sender_phone_number: Optional[str] = Field( sender_phone_number: Optional[str] = Field(
None, description="The sender's phone number, if available" None,
description="The sender's phone number, if available",
) )
sender_address: Optional[str] = Field( sender_address: Optional[str] = Field(
None, description="The sender's address, if available" None,
description="The sender's address, if available",
) )
action_items: list[str] = Field( action_items: list[str] = Field(
..., description="A list of action items requested by the email" ...,
description="A list of action items requested by the email",
) )
topic: str = Field( topic: str = Field(
..., description="High level description of what the email is about" ...,
description="High level description of what the email is about",
) )
tone: ToneEnum = Field(..., description="The tone of the email.") tone: ToneEnum = Field(..., description="The tone of the email.")
@ -150,7 +158,7 @@ def test_anthropic_complex_structured_output() -> None:
"human", "human",
"What can you tell me about the following email? Make sure to answer in the correct format: {email}", # noqa: E501 "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] llm = ChatAnthropicTools( # type: ignore[call-arg, call-arg]
@ -163,7 +171,7 @@ def test_anthropic_complex_structured_output() -> None:
response = extraction_chain.invoke( 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 "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) assert isinstance(response, Email)

View File

@ -1,4 +1,4 @@
"""Standard LangChain interface tests""" """Standard LangChain interface tests."""
from pathlib import Path from pathlib import Path
from typing import Literal, cast from typing import Literal, cast
@ -87,9 +87,9 @@ class TestAnthropicStandard(ChatModelIntegrationTests):
"type": "text", "type": "text",
"text": input_, "text": input_,
"cache_control": {"type": "ephemeral"}, "cache_control": {"type": "ephemeral"},
} },
], ],
} },
], ],
stream, stream,
) )
@ -118,9 +118,9 @@ class TestAnthropicStandard(ChatModelIntegrationTests):
"type": "text", "type": "text",
"text": input_, "text": input_,
"cache_control": {"type": "ephemeral"}, "cache_control": {"type": "ephemeral"},
} },
], ],
} },
], ],
stream, stream,
) )
@ -134,22 +134,18 @@ class TestAnthropicStandard(ChatModelIntegrationTests):
"type": "text", "type": "text",
"text": input_, "text": input_,
"cache_control": {"type": "ephemeral"}, "cache_control": {"type": "ephemeral"},
} },
], ],
} },
], ],
stream, stream,
) )
def _invoke(llm: ChatAnthropic, input_: list, stream: bool) -> AIMessage: def _invoke(llm: ChatAnthropic, input_: list, stream: bool) -> AIMessage: # noqa: FBT001
if stream: if stream:
full = None full = None
for chunk in llm.stream(input_): for chunk in llm.stream(input_):
if full is None: full = cast(BaseMessageChunk, chunk) if full is None else full + chunk
full = cast(BaseMessageChunk, chunk)
else:
full = full + chunk
return cast(AIMessage, full) return cast(AIMessage, full)
else: return cast(AIMessage, llm.invoke(input_))
return cast(AIMessage, llm.invoke(input_))

View File

@ -1,5 +1,7 @@
"""A fake callback handler for testing purposes.""" """A fake callback handler for testing purposes."""
from __future__ import annotations
from typing import Any, Union from typing import Any, Union
from langchain_core.callbacks import BaseCallbackHandler from langchain_core.callbacks import BaseCallbackHandler
@ -252,5 +254,5 @@ class FakeCallbackHandler(BaseCallbackHandler, BaseFakeCallbackHandlerMixin):
self.on_retriever_error_common() self.on_retriever_error_common()
# Overriding since BaseModel has __deepcopy__ method as well # Overriding since BaseModel has __deepcopy__ method as well
def __deepcopy__(self, memo: dict) -> "FakeCallbackHandler": # type: ignore def __deepcopy__(self, memo: dict) -> FakeCallbackHandler: # type: ignore[override]
return self return self

View File

@ -1,5 +1,7 @@
"""Test chat model integration.""" """Test chat model integration."""
from __future__ import annotations
import os import os
from typing import Any, Callable, Literal, Optional, cast from typing import Any, Callable, Literal, Optional, cast
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
@ -187,7 +189,7 @@ def test__merge_messages() -> None:
"text": None, "text": None,
"name": "blah", "name": "blah",
}, },
] ],
), ),
ToolMessage("buz output", tool_call_id="1", status="error"), # type: ignore[misc] ToolMessage("buz output", tool_call_id="1", status="error"), # type: ignore[misc]
ToolMessage( ToolMessage(
@ -234,7 +236,7 @@ def test__merge_messages() -> None:
"text": None, "text": None,
"name": "blah", "name": "blah",
}, },
] ],
), ),
HumanMessage( # type: ignore[misc] HumanMessage( # type: ignore[misc]
[ [
@ -266,7 +268,7 @@ def test__merge_messages() -> None:
"is_error": False, "is_error": False,
}, },
{"type": "text", "text": "next thing"}, {"type": "text", "text": "next thing"},
] ],
), ),
] ]
actual = _merge_messages(messages) actual = _merge_messages(messages)
@ -277,7 +279,7 @@ def test__merge_messages() -> None:
ToolMessage("buz output", tool_call_id="1"), # type: ignore[misc] ToolMessage("buz output", tool_call_id="1"), # type: ignore[misc]
ToolMessage( # type: ignore[misc] ToolMessage( # type: ignore[misc]
content=[ content=[
{"type": "tool_result", "content": "blah output", "tool_use_id": "2"} {"type": "tool_result", "content": "blah output", "tool_use_id": "2"},
], ],
tool_call_id="2", tool_call_id="2",
), ),
@ -292,8 +294,8 @@ def test__merge_messages() -> None:
"is_error": False, "is_error": False,
}, },
{"type": "tool_result", "content": "blah output", "tool_use_id": "2"}, {"type": "tool_result", "content": "blah output", "tool_use_id": "2"},
] ],
) ),
] ]
actual = _merge_messages(messages) actual = _merge_messages(messages)
assert expected == actual assert expected == actual
@ -310,7 +312,7 @@ def test__merge_messages_mutation() -> None:
] ]
expected = [ expected = [
HumanMessage( # type: ignore[misc] HumanMessage( # type: ignore[misc]
[{"type": "text", "text": "bar"}, {"type": "text", "text": "next thing"}] [{"type": "text", "text": "bar"}, {"type": "text", "text": "next thing"}],
), ),
] ]
actual = _merge_messages(messages) actual = _merge_messages(messages)
@ -327,7 +329,7 @@ def test__format_image() -> None:
@pytest.fixture() @pytest.fixture()
def pydantic() -> type[BaseModel]: def pydantic() -> type[BaseModel]:
class dummy_function(BaseModel): class dummy_function(BaseModel):
"""dummy function""" """Dummy function."""
arg1: int = Field(..., description="foo") arg1: int = Field(..., description="foo")
arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'") arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'")
@ -338,13 +340,14 @@ def pydantic() -> type[BaseModel]:
@pytest.fixture() @pytest.fixture()
def function() -> Callable: def function() -> Callable:
def dummy_function(arg1: int, arg2: Literal["bar", "baz"]) -> None: def dummy_function(arg1: int, arg2: Literal["bar", "baz"]) -> None:
"""dummy function """Dummy function.
Args: Args:
----
arg1: foo arg1: foo
arg2: one of 'bar', 'baz' arg2: one of 'bar', 'baz'
"""
pass """ # noqa: D401
return dummy_function return dummy_function
@ -358,7 +361,7 @@ def dummy_tool() -> BaseTool:
class DummyFunction(BaseTool): # type: ignore[override] class DummyFunction(BaseTool): # type: ignore[override]
args_schema: type[BaseModel] = Schema args_schema: type[BaseModel] = Schema
name: str = "dummy_function" name: str = "dummy_function"
description: str = "dummy function" description: str = "Dummy function."
def _run(self, *args: Any, **kwargs: Any) -> Any: def _run(self, *args: Any, **kwargs: Any) -> Any:
pass pass
@ -370,7 +373,7 @@ def dummy_tool() -> BaseTool:
def json_schema() -> dict: def json_schema() -> dict:
return { return {
"title": "dummy_function", "title": "dummy_function",
"description": "dummy function", "description": "Dummy function.",
"type": "object", "type": "object",
"properties": { "properties": {
"arg1": {"description": "foo", "type": "integer"}, "arg1": {"description": "foo", "type": "integer"},
@ -388,7 +391,7 @@ def json_schema() -> dict:
def openai_function() -> dict: def openai_function() -> dict:
return { return {
"name": "dummy_function", "name": "dummy_function",
"description": "dummy function", "description": "Dummy function.",
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -413,7 +416,7 @@ def test_convert_to_anthropic_tool(
) -> None: ) -> None:
expected = { expected = {
"name": "dummy_function", "name": "dummy_function",
"description": "dummy function", "description": "Dummy function.",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -429,7 +432,7 @@ def test_convert_to_anthropic_tool(
} }
for fn in (pydantic, function, dummy_tool, json_schema, expected, openai_function): for fn in (pydantic, function, dummy_tool, json_schema, expected, openai_function):
actual = convert_to_anthropic_tool(fn) # type: ignore actual = convert_to_anthropic_tool(fn)
assert actual == expected assert actual == expected
@ -461,7 +464,7 @@ def test__format_messages_with_tool_calls() -> None:
"type": "base64", "type": "base64",
"media_type": "image/jpeg", "media_type": "image/jpeg",
}, },
} },
], ],
tool_call_id="3", tool_call_id="3",
) )
@ -478,7 +481,7 @@ def test__format_messages_with_tool_calls() -> None:
"name": "bar", "name": "bar",
"id": "1", "id": "1",
"input": {"baz": "buzz"}, "input": {"baz": "buzz"},
} },
], ],
}, },
{ {
@ -489,7 +492,7 @@ def test__format_messages_with_tool_calls() -> None:
"content": "blurb", "content": "blurb",
"tool_use_id": "1", "tool_use_id": "1",
"is_error": False, "is_error": False,
} },
], ],
}, },
{ {
@ -500,7 +503,7 @@ def test__format_messages_with_tool_calls() -> None:
"name": "bar", "name": "bar",
"id": "2", "id": "2",
"input": {"baz": "buzz"}, "input": {"baz": "buzz"},
} },
], ],
}, },
{ {
@ -516,7 +519,7 @@ def test__format_messages_with_tool_calls() -> None:
"type": "base64", "type": "base64",
"media_type": "image/jpeg", "media_type": "image/jpeg",
}, },
} },
], ],
"tool_use_id": "2", "tool_use_id": "2",
"is_error": False, "is_error": False,
@ -531,7 +534,7 @@ def test__format_messages_with_tool_calls() -> None:
"type": "base64", "type": "base64",
"media_type": "image/jpeg", "media_type": "image/jpeg",
}, },
} },
], ],
"tool_use_id": "3", "tool_use_id": "3",
"is_error": False, "is_error": False,
@ -579,7 +582,7 @@ def test__format_messages_with_str_content_and_tool_calls() -> None:
"content": "blurb", "content": "blurb",
"tool_use_id": "1", "tool_use_id": "1",
"is_error": False, "is_error": False,
} },
], ],
}, },
], ],
@ -624,7 +627,7 @@ def test__format_messages_with_list_content_and_tool_calls() -> None:
"content": "blurb", "content": "blurb",
"tool_use_id": "1", "tool_use_id": "1",
"is_error": False, "is_error": False,
} },
], ],
}, },
], ],
@ -676,7 +679,7 @@ def test__format_messages_with_tool_use_blocks_and_tool_calls() -> None:
"content": "blurb", "content": "blurb",
"tool_use_id": "1", "tool_use_id": "1",
"is_error": False, "is_error": False,
} },
], ],
}, },
], ],
@ -690,7 +693,7 @@ def test__format_messages_with_cache_control() -> None:
SystemMessage( SystemMessage(
[ [
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}}, {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
] ],
), ),
HumanMessage( HumanMessage(
[ [
@ -699,11 +702,11 @@ def test__format_messages_with_cache_control() -> None:
"type": "text", "type": "text",
"text": "foo", "text": "foo",
}, },
] ],
), ),
] ]
expected_system = [ expected_system = [
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}} {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
] ]
expected_messages = [ expected_messages = [
{ {
@ -712,7 +715,7 @@ def test__format_messages_with_cache_control() -> None:
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}}, {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
{"type": "text", "text": "foo"}, {"type": "text", "text": "foo"},
], ],
} },
] ]
actual_system, actual_messages = _format_messages(messages) actual_system, actual_messages = _format_messages(messages)
assert expected_system == actual_system assert expected_system == actual_system
@ -733,8 +736,8 @@ def test__format_messages_with_cache_control() -> None:
"data": "<base64 data>", "data": "<base64 data>",
"cache_control": {"type": "ephemeral"}, "cache_control": {"type": "ephemeral"},
}, },
] ],
) ),
] ]
actual_system, actual_messages = _format_messages(messages) actual_system, actual_messages = _format_messages(messages)
assert actual_system is None assert actual_system is None
@ -756,7 +759,7 @@ def test__format_messages_with_cache_control() -> None:
"cache_control": {"type": "ephemeral"}, "cache_control": {"type": "ephemeral"},
}, },
], ],
} },
] ]
assert actual_messages == expected_messages assert actual_messages == expected_messages
@ -773,8 +776,8 @@ def test__format_messages_with_citations() -> None:
"citations": {"enabled": True}, "citations": {"enabled": True},
}, },
{"type": "text", "text": "What color is the grass and sky?"}, {"type": "text", "text": "What color is the grass and sky?"},
] ],
) ),
] ]
expected_messages = [ expected_messages = [
{ {
@ -791,7 +794,7 @@ def test__format_messages_with_citations() -> None:
}, },
{"type": "text", "text": "What color is the grass and sky?"}, {"type": "text", "text": "What color is the grass and sky?"},
], ],
} },
] ]
actual_system, actual_messages = _format_messages(input_messages) actual_system, actual_messages = _format_messages(input_messages)
assert actual_system is None assert actual_system is None
@ -843,7 +846,7 @@ def test__format_messages_openai_image_format() -> None:
}, },
}, },
], ],
} },
] ]
assert actual_messages == expected_messages assert actual_messages == expected_messages
@ -856,7 +859,7 @@ def test__format_messages_with_multiple_system() -> None:
SystemMessage( SystemMessage(
[ [
{"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}}, {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}},
] ],
), ),
] ]
expected_system = [ expected_system = [
@ -880,7 +883,8 @@ def test_anthropic_api_key_is_secret_string() -> None:
def test_anthropic_api_key_masked_when_passed_from_env( def test_anthropic_api_key_masked_when_passed_from_env(
monkeypatch: MonkeyPatch, capsys: CaptureFixture monkeypatch: MonkeyPatch,
capsys: CaptureFixture,
) -> None: ) -> None:
"""Test that the API key is masked when passed from an environment variable.""" """Test that the API key is masked when passed from an environment variable."""
monkeypatch.setenv("ANTHROPIC_API_KEY ", "secret-api-key") monkeypatch.setenv("ANTHROPIC_API_KEY ", "secret-api-key")
@ -920,7 +924,7 @@ def test_anthropic_uses_actual_secret_value_from_secretstr() -> None:
class GetWeather(BaseModel): class GetWeather(BaseModel):
"""Get the current weather in a given location""" """Get the current weather in a given location."""
location: str = Field(..., description="The city and state, e.g. San Francisco, CA") location: str = Field(..., description="The city and state, e.g. San Francisco, CA")
@ -931,14 +935,16 @@ def test_anthropic_bind_tools_tool_choice() -> None:
anthropic_api_key="secret-api-key", anthropic_api_key="secret-api-key",
) )
chat_model_with_tools = chat_model.bind_tools( chat_model_with_tools = chat_model.bind_tools(
[GetWeather], tool_choice={"type": "tool", "name": "GetWeather"} [GetWeather],
tool_choice={"type": "tool", "name": "GetWeather"},
) )
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == { assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
"type": "tool", "type": "tool",
"name": "GetWeather", "name": "GetWeather",
} }
chat_model_with_tools = chat_model.bind_tools( chat_model_with_tools = chat_model.bind_tools(
[GetWeather], tool_choice="GetWeather" [GetWeather],
tool_choice="GetWeather",
) )
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == { assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
"type": "tool", "type": "tool",
@ -946,11 +952,11 @@ def test_anthropic_bind_tools_tool_choice() -> None:
} }
chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="auto") chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="auto")
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == { assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
"type": "auto" "type": "auto",
} }
chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="any") chat_model_with_tools = chat_model.bind_tools([GetWeather], tool_choice="any")
assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == { assert cast(RunnableBinding, chat_model_with_tools).kwargs["tool_choice"] == {
"type": "any" "type": "any",
} }
@ -1021,7 +1027,6 @@ class FakeTracer(BaseTracer):
def _persist_run(self, run: Run) -> None: def _persist_run(self, run: Run) -> None:
"""Persist a run.""" """Persist a run."""
pass
def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run: def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run:
self.chat_model_start_inputs.append({"args": args, "kwargs": kwargs}) self.chat_model_start_inputs.append({"args": args, "kwargs": kwargs})
@ -1036,7 +1041,7 @@ def test_mcp_tracing() -> None:
"url": "https://mcp.deepwiki.com/mcp", "url": "https://mcp.deepwiki.com/mcp",
"name": "deepwiki", "name": "deepwiki",
"authorization_token": "PLACEHOLDER", "authorization_token": "PLACEHOLDER",
} },
] ]
llm = ChatAnthropic( llm = ChatAnthropic(

View File

@ -95,7 +95,8 @@ def test_tools_output_parser_empty_content() -> None:
chart_type: Literal["pie", "line", "bar"] chart_type: Literal["pie", "line", "bar"]
output_parser = ToolsOutputParser( output_parser = ToolsOutputParser(
first_tool_only=True, pydantic_schemas=[ChartType] first_tool_only=True,
pydantic_schemas=[ChartType],
) )
message = AIMessage( message = AIMessage(
"", "",
@ -105,7 +106,7 @@ def test_tools_output_parser_empty_content() -> None:
"args": {"chart_type": "pie"}, "args": {"chart_type": "pie"},
"id": "foo", "id": "foo",
"type": "tool_call", "type": "tool_call",
} },
], ],
) )
actual = output_parser.invoke(message) actual = output_parser.invoke(message)

View File

@ -1,4 +1,4 @@
"""Standard LangChain interface tests""" """Standard LangChain interface tests."""
import pytest import pytest
from langchain_core.language_models import BaseChatModel from langchain_core.language_models import BaseChatModel

View File

@ -477,7 +477,7 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }] codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
dev = [{ name = "langchain-core", editable = "../../core" }] dev = [{ name = "langchain-core", editable = "../../core" }]
lint = [{ name = "ruff", specifier = ">=0.5,<1.0" }] lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
test = [ test = [
{ name = "defusedxml", specifier = ">=0.7.1,<1.0.0" }, { name = "defusedxml", specifier = ">=0.7.1,<1.0.0" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" }, { name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
@ -534,7 +534,7 @@ dev = [
{ name = "jupyter", specifier = ">=1.0.0,<2.0.0" }, { name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" }, { name = "setuptools", specifier = ">=67.6.1,<68.0.0" },
] ]
lint = [{ name = "ruff", specifier = ">=0.11.2,<0.12.0" }] lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
test = [ test = [
{ name = "blockbuster", specifier = "~=1.5.18" }, { name = "blockbuster", specifier = "~=1.5.18" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" }, { name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
@ -598,7 +598,7 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }] codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
lint = [{ name = "ruff", specifier = ">=0.9.2,<1.0.0" }] lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
test = [{ name = "langchain-core", editable = "../../core" }] test = [{ name = "langchain-core", editable = "../../core" }]
test-integration = [] test-integration = []
typing = [ typing = [
@ -1513,27 +1513,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.5.7" version = "0.12.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/2b/69e5e412f9d390adbdbcbf4f64d6914fa61b44b08839a6584655014fc524/ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5", size = 2449817, upload-time = "2024-08-08T15:43:07.467Z" } sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/eb/06e06aaf96af30a68e83b357b037008c54a2ddcbad4f989535007c700394/ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a", size = 9570571, upload-time = "2024-08-08T15:41:56.537Z" }, { url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
{ url = "https://files.pythonhosted.org/packages/a4/10/1be32aeaab8728f78f673e7a47dd813222364479b2d6573dbcf0085e83ea/ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be", size = 8685138, upload-time = "2024-08-08T15:42:02.833Z" }, { url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
{ url = "https://files.pythonhosted.org/packages/3d/1d/c218ce83beb4394ba04d05e9aa2ae6ce9fba8405688fe878b0fdb40ce855/ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e", size = 8266785, upload-time = "2024-08-08T15:42:08.321Z" }, { url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
{ url = "https://files.pythonhosted.org/packages/26/79/7f49509bd844476235b40425756def366b227a9714191c91f02fb2178635/ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8", size = 9983964, upload-time = "2024-08-08T15:42:12.419Z" }, { url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
{ url = "https://files.pythonhosted.org/packages/bf/b1/939836b70bf9fcd5e5cd3ea67fdb8abb9eac7631351d32f26544034a35e4/ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea", size = 9359490, upload-time = "2024-08-08T15:42:16.713Z" }, { url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
{ url = "https://files.pythonhosted.org/packages/32/7d/b3db19207de105daad0c8b704b2c6f2a011f9c07017bd58d8d6e7b8eba19/ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc", size = 10170833, upload-time = "2024-08-08T15:42:20.54Z" }, { url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
{ url = "https://files.pythonhosted.org/packages/a2/45/eae9da55f3357a1ac04220230b8b07800bf516e6dd7e1ad20a2ff3b03b1b/ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692", size = 10896360, upload-time = "2024-08-08T15:42:25.2Z" }, { url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
{ url = "https://files.pythonhosted.org/packages/99/67/4388b36d145675f4c51ebec561fcd4298a0e2550c81e629116f83ce45a39/ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf", size = 10477094, upload-time = "2024-08-08T15:42:29.553Z" }, { url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
{ url = "https://files.pythonhosted.org/packages/e1/9c/f5e6ed1751dc187a4ecf19a4970dd30a521c0ee66b7941c16e292a4043fb/ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb", size = 11480896, upload-time = "2024-08-08T15:42:33.772Z" }, { url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
{ url = "https://files.pythonhosted.org/packages/c8/3b/2b683be597bbd02046678fc3fc1c199c641512b20212073b58f173822bb3/ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e", size = 10179702, upload-time = "2024-08-08T15:42:38.038Z" }, { url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
{ url = "https://files.pythonhosted.org/packages/f1/38/c2d94054dc4b3d1ea4c2ba3439b2a7095f08d1c8184bc41e6abe2a688be7/ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499", size = 9982855, upload-time = "2024-08-08T15:42:42.031Z" }, { url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
{ url = "https://files.pythonhosted.org/packages/7d/e7/1433db2da505ffa8912dcf5b28a8743012ee780cbc20ad0bf114787385d9/ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e", size = 9433156, upload-time = "2024-08-08T15:42:45.339Z" }, { url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
{ url = "https://files.pythonhosted.org/packages/e0/36/4fa43250e67741edeea3d366f59a1dc993d4d89ad493a36cbaa9889895f2/ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5", size = 9782971, upload-time = "2024-08-08T15:42:49.354Z" }, { url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
{ url = "https://files.pythonhosted.org/packages/80/0e/8c276103d518e5cf9202f70630aaa494abf6fc71c04d87c08b6d3cd07a4b/ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e", size = 10247775, upload-time = "2024-08-08T15:42:53.294Z" }, { url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
{ url = "https://files.pythonhosted.org/packages/cb/b9/673096d61276f39291b729dddde23c831a5833d98048349835782688a0ec/ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a", size = 7841772, upload-time = "2024-08-08T15:42:57.488Z" }, { url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
{ url = "https://files.pythonhosted.org/packages/67/1c/4520c98bfc06b9c73cd1457686d4d3935d40046b1ddea08403e5a6deff51/ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3", size = 8699779, upload-time = "2024-08-08T15:43:00.429Z" }, { url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
{ url = "https://files.pythonhosted.org/packages/38/23/b3763a237d2523d40a31fe2d1a301191fe392dd48d3014977d079cf8c0bd/ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4", size = 8091891, upload-time = "2024-08-08T15:43:04.162Z" }, { url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
] ]
[[package]] [[package]]