diff --git a/libs/langchain/pyproject.toml b/libs/langchain/pyproject.toml index 6aefa38a9c3..d87d7f37219 100644 --- a/libs/langchain/pyproject.toml +++ b/libs/langchain/pyproject.toml @@ -180,13 +180,13 @@ select = [ "YTT", # flake8-2020 ] ignore = [ - "D100", - "D101", - "D102", - "D103", - "D104", - "D105", - "D107", + "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__ "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful diff --git a/libs/partners/anthropic/langchain_anthropic/__init__.py b/libs/partners/anthropic/langchain_anthropic/__init__.py index 4dcf4eb1878..399fece0c2d 100644 --- a/libs/partners/anthropic/langchain_anthropic/__init__.py +++ b/libs/partners/anthropic/langchain_anthropic/__init__.py @@ -6,9 +6,9 @@ from langchain_anthropic.chat_models import ( from langchain_anthropic.llms import Anthropic, AnthropicLLM __all__ = [ - "ChatAnthropicMessages", - "ChatAnthropic", - "convert_to_anthropic_tool", "Anthropic", "AnthropicLLM", + "ChatAnthropic", + "ChatAnthropicMessages", + "convert_to_anthropic_tool", ] diff --git a/libs/partners/anthropic/langchain_anthropic/_client_utils.py b/libs/partners/anthropic/langchain_anthropic/_client_utils.py index a82e5655a9c..40591107fb6 100644 --- a/libs/partners/anthropic/langchain_anthropic/_client_utils.py +++ b/libs/partners/anthropic/langchain_anthropic/_client_utils.py @@ -6,6 +6,8 @@ for each instance of ChatAnthropic. Logic is largely replicated from anthropic._base_client. """ +from __future__ import annotations + import asyncio import os from functools import lru_cache @@ -17,7 +19,7 @@ _NOT_GIVEN: Any = object() class _SyncHttpxClientWrapper(anthropic.DefaultHttpxClient): - """Borrowed from anthropic._base_client""" + """Borrowed from anthropic._base_client.""" def __del__(self) -> None: if self.is_closed: @@ -30,7 +32,7 @@ class _SyncHttpxClientWrapper(anthropic.DefaultHttpxClient): class _AsyncHttpxClientWrapper(anthropic.DefaultAsyncHttpxClient): - """Borrowed from anthropic._base_client""" + """Borrowed from anthropic._base_client.""" def __del__(self) -> None: if self.is_closed: diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index 67fe22c4824..cf6017f99b3 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import copy import json import re @@ -111,18 +113,19 @@ def _is_builtin_tool(tool: Any) -> bool: def _format_image(url: str) -> dict: - """ - Converts part["image_url"]["url"] strings (OpenAI format) - to the correct Anthropic format: + """Convert part["image_url"]["url"] strings (OpenAI format) to Anthropic format. + { - "type": "base64", - "media_type": "image/jpeg", - "data": "/9j/4AAQSkZJRg...", + "type": "base64", + "media_type": "image/jpeg", + "data": "/9j/4AAQSkZJRg...", } + Or + { - "type": "url", - "url": "https://example.com/image.jpg", + "type": "url", + "url": "https://example.com/image.jpg", } """ # Base64 encoded image @@ -146,11 +149,14 @@ def _format_image(url: str) -> dict: "url": url, } - raise ValueError( + msg = ( "Malformed url parameter." " Must be either an image URL (https://example.com/image.jpg)" " or base64 encoded string (data:image/png;base64,'/9j/4AAQSk'...)" ) + raise ValueError( + msg, + ) def _merge_messages( @@ -177,8 +183,8 @@ def _merge_messages( "content": curr.content, "tool_use_id": curr.tool_call_id, "is_error": curr.status == "error", - } - ] + }, + ], ) last = merged[-1] if merged else None if any( @@ -187,7 +193,7 @@ def _merge_messages( ): if isinstance(cast(BaseMessage, last).content, str): new_content: list = [ - {"type": "text", "text": cast(BaseMessage, last).content} + {"type": "text", "text": cast(BaseMessage, last).content}, ] else: new_content = copy.copy(cast(list, cast(BaseMessage, last).content)) @@ -234,10 +240,13 @@ def _format_data_content_block(block: dict) -> dict: }, } else: - raise ValueError( + msg = ( "Anthropic only supports 'url' and 'base64' source_type for image " "content blocks." ) + raise ValueError( + msg, + ) elif block["type"] == "file": if block["source_type"] == "url": @@ -276,7 +285,8 @@ def _format_data_content_block(block: dict) -> dict: } 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: for key in ["cache_control", "citations", "title", "context"]: @@ -292,7 +302,6 @@ def _format_messages( messages: Sequence[BaseMessage], ) -> tuple[Union[str, list[dict], None], list[dict]]: """Format messages for anthropic.""" - """ [ { @@ -308,8 +317,9 @@ def _format_messages( for i, message in enumerate(merged_messages): if message.type == "system": if system is not None: - raise ValueError("Received multiple non-consecutive system messages.") - elif isinstance(message.content, list): + msg = "Received multiple non-consecutive system messages." + raise ValueError(msg) + if isinstance(message.content, list): system = [ ( block @@ -328,8 +338,9 @@ def _format_messages( if not isinstance(message.content, str): # parse as dict if not isinstance(message.content, list): + msg = "Anthropic message content must be str or list of dicts" raise ValueError( - "Anthropic message content must be str or list of dicts" + msg, ) # populate content @@ -339,8 +350,9 @@ def _format_messages( content.append({"type": "text", "text": block}) elif isinstance(block, dict): if "type" not in block: - raise ValueError("Dict content block must have a type key") - elif block["type"] == "image_url": + msg = "Dict content block must have a type key" + raise ValueError(msg) + if block["type"] == "image_url": # convert format source = _format_image(block["image_url"]["url"]) content.append({"type": "image", "source": source}) @@ -358,7 +370,9 @@ def _format_messages( if tc["id"] == block["id"] ] content.extend( - _lc_tool_calls_to_anthropic_tool_use_blocks(overlapping) + _lc_tool_calls_to_anthropic_tool_use_blocks( + overlapping, + ), ) else: block.pop("text", None) @@ -398,7 +412,7 @@ def _format_messages( for k, v in block.items() if k in ("type", "text", "cache_control", "citations") - } + }, ) elif block["type"] == "thinking": content.append( @@ -407,7 +421,7 @@ def _format_messages( for k, v in block.items() if k in ("type", "thinking", "cache_control", "signature") - } + }, ) elif block["type"] == "redacted_thinking": content.append( @@ -415,13 +429,13 @@ def _format_messages( k: v for k, v in block.items() if k in ("type", "cache_control", "data") - } + }, ) elif block["type"] == "tool_result": tool_content = _format_messages( - [HumanMessage(block["content"])] + [HumanMessage(block["content"])], )[1][0]["content"] - content.append({**block, **{"content": tool_content}}) + content.append({**block, "content": tool_content}) elif block["type"] in ( "code_execution_tool_result", "mcp_tool_result", @@ -439,15 +453,18 @@ def _format_messages( "is_error", # for mcp_tool_result "cache_control", ) - } + }, ) else: content.append(block) else: - raise ValueError( + msg = ( f"Content blocks must be str or dict, instead was: " f"{type(block)}" ) + raise ValueError( + msg, + ) else: 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 ] 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}) @@ -481,8 +498,7 @@ def _handle_anthropic_bad_request(e: anthropic.BadRequestError) -> None: message = "Received only system message(s). " warnings.warn(message) raise e - else: - raise + raise class ChatAnthropic(BaseChatModel): @@ -635,17 +651,17 @@ class ChatAnthropic(BaseChatModel): .. code-block:: python [{'name': 'GetWeather', - 'args': {'location': 'Los Angeles, CA'}, - 'id': 'toolu_01KzpPEAgzura7hpBqwHbWdo'}, + 'args': {'location': 'Los Angeles, CA'}, + 'id': 'toolu_01KzpPEAgzura7hpBqwHbWdo'}, {'name': 'GetWeather', - 'args': {'location': 'New York, NY'}, - 'id': 'toolu_01JtgbVGVJbiSwtZk3Uycezx'}, + 'args': {'location': 'New York, NY'}, + 'id': 'toolu_01JtgbVGVJbiSwtZk3Uycezx'}, {'name': 'GetPopulation', - 'args': {'location': 'Los Angeles, CA'}, - 'id': 'toolu_01429aygngesudV9nTbCKGuw'}, + 'args': {'location': 'Los Angeles, CA'}, + 'id': 'toolu_01429aygngesudV9nTbCKGuw'}, {'name': 'GetPopulation', - 'args': {'location': 'New York, NY'}, - 'id': 'toolu_01JPktyd44tVMeBcPPnFSEJG'}] + 'args': {'location': 'New York, NY'}, + 'id': 'toolu_01JPktyd44tVMeBcPPnFSEJG'}] See ``ChatAnthropic.bind_tools()`` method for more. @@ -673,7 +689,7 @@ class ChatAnthropic(BaseChatModel): See ``ChatAnthropic.with_structured_output()`` for more. Image input: - See `multimodal guides `_ + See `multimodal guides `__ for more detail. .. code-block:: python @@ -717,7 +733,7 @@ class ChatAnthropic(BaseChatModel): .. dropdown:: Files API You can also pass in files that are managed through Anthropic's - `Files API `_: + `Files API `__: .. code-block:: python @@ -744,7 +760,7 @@ class ChatAnthropic(BaseChatModel): llm.invoke([input_message]) PDF input: - See `multimodal guides `_ + See `multimodal guides `__ for more detail. .. code-block:: python @@ -782,7 +798,7 @@ class ChatAnthropic(BaseChatModel): .. dropdown:: Files API You can also pass in files that are managed through Anthropic's - `Files API `_: + `Files API `__: .. code-block:: python @@ -810,7 +826,7 @@ class ChatAnthropic(BaseChatModel): Extended thinking: Claude 3.7 Sonnet supports an - `extended thinking `_ + `extended thinking `__ feature, which will output the step-by-step reasoning process that led to its final answer. @@ -838,10 +854,10 @@ class ChatAnthropic(BaseChatModel): Citations: Anthropic supports a - `citations `_ + `citations `__ feature that lets Claude attach context to its answers based on source documents supplied by the user. When - `document content blocks `_ + `document content blocks `__ with ``"citations": {"enabled": True}`` are included in a query, Claude may generate citations in its response. @@ -924,7 +940,7 @@ class ChatAnthropic(BaseChatModel): or by setting ``stream_usage=False`` when initializing ChatAnthropic. Prompt caching: - See LangChain `docs `_ + See LangChain `docs `__ for more detail. .. code-block:: python @@ -1000,11 +1016,11 @@ class ChatAnthropic(BaseChatModel): } } - See `Claude documentation `_ + See `Claude documentation `__ for detail. Token-efficient tool use (beta): - See LangChain `docs `_ + See LangChain `docs `__ for more detail. .. code-block:: python @@ -1041,7 +1057,7 @@ class ChatAnthropic(BaseChatModel): Total tokens: 408 Built-in tools: - See LangChain `docs `_ + See LangChain `docs `__ for more detail. .. dropdown:: Web search @@ -1266,7 +1282,9 @@ class ChatAnthropic(BaseChatModel): } def _get_ls_params( - self, stop: Optional[list[str]] = None, **kwargs: Any + self, + stop: Optional[list[str]] = None, + **kwargs: Any, ) -> LangSmithParams: """Get standard params for tracing.""" params = self._get_invocation_params(stop=stop, **kwargs) @@ -1286,8 +1304,7 @@ class ChatAnthropic(BaseChatModel): @classmethod def build_extra(cls, values: dict) -> Any: all_required_field_names = get_pydantic_field_names(cls) - values = _build_model_kwargs(values, all_required_field_names) - return values + return _build_model_kwargs(values, all_required_field_names) @cached_property def _client_params(self) -> dict[str, Any]: @@ -1361,14 +1378,12 @@ class ChatAnthropic(BaseChatModel): def _create(self, payload: dict) -> Any: if "betas" in 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: if "betas" in 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( self, @@ -1496,7 +1511,10 @@ class ChatAnthropic(BaseChatModel): ) -> ChatResult: if self.streaming: 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) payload = self._get_request_payload(messages, stop=stop, **kwargs) @@ -1515,7 +1533,10 @@ class ChatAnthropic(BaseChatModel): ) -> ChatResult: if self.streaming: 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) 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]], *, tool_choice: Optional[ - Union[dict[str, str], Literal["any", "auto"], str] + Union[dict[str, str], Literal["any", "auto"], str] # noqa: PYI051 ] = None, parallel_tool_calls: Optional[bool] = None, **kwargs: Any, @@ -1716,10 +1737,13 @@ class ChatAnthropic(BaseChatModel): elif isinstance(tool_choice, str): kwargs["tool_choice"] = {"type": "tool", "name": tool_choice} else: - raise ValueError( + msg = ( f"Unrecognized 'tool_choice' type {tool_choice=}. Expected dict, " f"str, or None." ) + raise ValueError( + msg, + ) if parallel_tool_calls is not None: disable_parallel_tool_use = not parallel_tool_calls @@ -1861,7 +1885,8 @@ class ChatAnthropic(BaseChatModel): tool_name = formatted_tool["name"] if self.thinking is not None and self.thinking.get("type") == "enabled": llm = self._get_llm_for_structured_output_when_thinking_is_enabled( - schema, formatted_tool + schema, + formatted_tool, ) else: llm = self.bind_tools( @@ -1875,24 +1900,27 @@ class ChatAnthropic(BaseChatModel): if isinstance(schema, type) and is_basemodel_subclass(schema): output_parser: OutputParserLike = PydanticToolsParser( - tools=[schema], first_tool_only=True + tools=[schema], + first_tool_only=True, ) else: output_parser = JsonOutputKeyToolsParser( - key_name=tool_name, first_tool_only=True + key_name=tool_name, + first_tool_only=True, ) if include_raw: 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_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 - else: - return llm | output_parser + return llm | output_parser @beta() def get_num_tokens_from_messages( @@ -1909,6 +1937,8 @@ class ChatAnthropic(BaseChatModel): messages: The message inputs to tokenize. tools: If provided, sequence of dict, BaseModel, function, or BaseTools to be converted to tool schemas. + kwargs: Additional keyword arguments are passed to the + :meth:`~langchain_anthropic.chat_models.ChatAnthropic.bind` method. Basic usage: @@ -1985,7 +2015,7 @@ def convert_to_anthropic_tool( if isinstance(tool, dict) and all( k in tool for k in ("name", "description", "input_schema") ): - anthropic_formatted = AnthropicTool(tool) # type: ignore + anthropic_formatted = AnthropicTool(tool) # type: ignore[misc] else: oai_formatted = convert_to_openai_tool(tool)["function"] anthropic_formatted = AnthropicTool( @@ -2032,17 +2062,15 @@ class _AnthropicToolUse(TypedDict): def _lc_tool_calls_to_anthropic_tool_use_blocks( tool_calls: list[ToolCall], ) -> list[_AnthropicToolUse]: - blocks = [] - for tool_call in tool_calls: - blocks.append( - _AnthropicToolUse( - type="tool_use", - name=tool_call["name"], - input=tool_call["args"], - id=cast(str, tool_call["id"]), - ) + return [ + _AnthropicToolUse( + type="tool_use", + name=tool_call["name"], + 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( @@ -2107,7 +2135,7 @@ def _make_message_chunk_from_anthropic_event( tool_call_chunks = [] message_chunk = AIMessageChunk( content=[content_block], - tool_call_chunks=tool_call_chunks, # type: ignore + tool_call_chunks=tool_call_chunks, ) block_start_event = event elif event.type == "content_block_delta": @@ -2122,14 +2150,10 @@ def _make_message_chunk_from_anthropic_event( if "citation" in content_block: content_block["citations"] = [content_block.pop("citation")] message_chunk = AIMessageChunk(content=[content_block]) - elif event.delta.type == "thinking_delta": - content_block = event.delta.model_dump() - if "text" in content_block and content_block["text"] is None: - 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": + elif ( + event.delta.type == "thinking_delta" + or event.delta.type == "signature_delta" + ): content_block = event.delta.model_dump() if "text" in content_block and content_block["text"] is None: content_block.pop("text") @@ -2155,7 +2179,7 @@ def _make_message_chunk_from_anthropic_event( tool_call_chunks = [] message_chunk = AIMessageChunk( 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: usage_metadata = UsageMetadata( diff --git a/libs/partners/anthropic/langchain_anthropic/experimental.py b/libs/partners/anthropic/langchain_anthropic/experimental.py index 5f4c8a4d23c..ec30ddcf687 100644 --- a/libs/partners/anthropic/langchain_anthropic/experimental.py +++ b/libs/partners/anthropic/langchain_anthropic/experimental.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json from typing import ( Any, @@ -66,7 +68,7 @@ def get_system_message(tools: list[dict]) -> str: parameter_description=parameter.get("description"), ) for name, parameter in tool["parameters"]["properties"].items() - ] + ], ), } for tool in tools @@ -79,7 +81,7 @@ def get_system_message(tools: list[dict]) -> str: formatted_parameters=tool["formatted_parameters"], ) for tool in tools_data - ] + ], ) 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): tool = filtered_tools[0] for key, value in arguments.items(): - if key in tool["parameters"]["properties"]: - if "type" in tool["parameters"]["properties"][key]: - if tool["parameters"]["properties"][key][ - "type" - ] == "array" and not isinstance(value, list): - arguments[key] = [value] - if ( - tool["parameters"]["properties"][key]["type"] != "object" - and isinstance(value, dict) - and len(value.keys()) == 1 - ): - arguments[key] = list(value.values())[0] + if ( + key in tool["parameters"]["properties"] + and "type" in tool["parameters"]["properties"][key] + ): + if tool["parameters"]["properties"][key][ + "type" + ] == "array" and not isinstance(value, list): + arguments[key] = [value] + if ( + tool["parameters"]["properties"][key]["type"] != "object" + and isinstance(value, dict) + and len(value.keys()) == 1 + ): + arguments[key] = next(iter(value.values())) return { "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]]: - """ - 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") return [_xml_to_function_call(invoke, tools) for invoke in invokes] diff --git a/libs/partners/anthropic/langchain_anthropic/llms.py b/libs/partners/anthropic/langchain_anthropic/llms.py index cc2c0777b1f..772a1ea1a8c 100644 --- a/libs/partners/anthropic/langchain_anthropic/llms.py +++ b/libs/partners/anthropic/langchain_anthropic/llms.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re import warnings from collections.abc import AsyncIterator, Iterator, Mapping @@ -85,8 +87,7 @@ class _AnthropicCommon(BaseLanguageModel): @classmethod def build_extra(cls, values: dict) -> Any: all_required_field_names = get_pydantic_field_names(cls) - values = _build_model_kwargs(values, all_required_field_names) - return values + return _build_model_kwargs(values, all_required_field_names) @model_validator(mode="after") def validate_environment(self) -> Self: @@ -125,11 +126,12 @@ class _AnthropicCommon(BaseLanguageModel): @property def _identifying_params(self) -> Mapping[str, Any]: """Get the identifying parameters.""" - return {**{}, **self._default_params} + return {**self._default_params} def _get_anthropic_stop(self, stop: Optional[list[str]] = None) -> list[str]: 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: stop = [] @@ -152,6 +154,7 @@ class AnthropicLLM(LLM, _AnthropicCommon): from langchain_anthropic import AnthropicLLM model = AnthropicLLM() + """ model_config = ConfigDict( @@ -166,7 +169,7 @@ class AnthropicLLM(LLM, _AnthropicCommon): warnings.warn( "This Anthropic LLM is deprecated. " "Please use `from langchain_anthropic import ChatAnthropic` " - "instead" + "instead", ) return values @@ -199,7 +202,9 @@ class AnthropicLLM(LLM, _AnthropicCommon): } def _get_ls_params( - self, stop: Optional[list[str]] = None, **kwargs: Any + self, + stop: Optional[list[str]] = None, + **kwargs: Any, ) -> LangSmithParams: """Get standard params for tracing.""" params = super()._get_ls_params(stop=stop, **kwargs) @@ -213,7 +218,8 @@ class AnthropicLLM(LLM, _AnthropicCommon): def _wrap_prompt(self, prompt: str) -> str: 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): return prompt # Already wrapped. @@ -238,6 +244,8 @@ class AnthropicLLM(LLM, _AnthropicCommon): Args: prompt: The prompt to pass into the model. 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: The string generated by the model. @@ -253,7 +261,10 @@ class AnthropicLLM(LLM, _AnthropicCommon): if self.streaming: completion = "" 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 return completion @@ -281,7 +292,10 @@ class AnthropicLLM(LLM, _AnthropicCommon): if self.streaming: completion = "" 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 return completion @@ -308,8 +322,12 @@ class AnthropicLLM(LLM, _AnthropicCommon): Args: prompt: The prompt to pass into the model. 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: A generator representing the stream of tokens from Anthropic. + Example: .. code-block:: python @@ -319,12 +337,16 @@ class AnthropicLLM(LLM, _AnthropicCommon): generator = anthropic.stream(prompt) for token in generator: yield token + """ stop = self._get_anthropic_stop(stop) params = {**self._default_params, **kwargs} 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) @@ -344,8 +366,12 @@ class AnthropicLLM(LLM, _AnthropicCommon): Args: prompt: The prompt to pass into the model. 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: A generator representing the stream of tokens from Anthropic. + Example: .. code-block:: python @@ -354,6 +380,7 @@ class AnthropicLLM(LLM, _AnthropicCommon): generator = anthropic.stream(prompt) for token in generator: yield token + """ stop = self._get_anthropic_stop(stop) params = {**self._default_params, **kwargs} @@ -372,15 +399,16 @@ class AnthropicLLM(LLM, _AnthropicCommon): def get_num_tokens(self, text: str) -> int: """Calculate number of tokens.""" - raise NotImplementedError( + msg = ( "Anthropic's legacy count_tokens method was removed in anthropic 0.39.0 " "and langchain-anthropic 0.3.0. Please use " "ChatAnthropic.get_num_tokens_from_messages instead." ) + raise NotImplementedError( + msg, + ) @deprecated(since="0.1.0", removal="1.0.0", alternative="AnthropicLLM") class Anthropic(AnthropicLLM): """Anthropic large language model.""" - - pass diff --git a/libs/partners/anthropic/langchain_anthropic/output_parsers.py b/libs/partners/anthropic/langchain_anthropic/output_parsers.py index 58c052cd175..83b8b0e8779 100644 --- a/libs/partners/anthropic/langchain_anthropic/output_parsers.py +++ b/libs/partners/anthropic/langchain_anthropic/output_parsers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any, Optional, Union, cast from langchain_core.messages import AIMessage, ToolCall @@ -27,8 +29,12 @@ class ToolsOutputParser(BaseGenerationOutputParser): Args: result: A list of Generations to be parsed. The Generations are assumed 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: Structured output. + """ if not result or not isinstance(result[0], ChatGeneration): return None if self.first_tool_only else [] @@ -53,8 +59,7 @@ class ToolsOutputParser(BaseGenerationOutputParser): if self.first_tool_only: return tool_calls[0] if tool_calls else None - else: - return [tool_call for tool_call in tool_calls] + return list(tool_calls) def _pydantic_parse(self, tool_call: dict) -> BaseModel: 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": continue 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 - else: - return [] + return [] diff --git a/libs/partners/anthropic/pyproject.toml b/libs/partners/anthropic/pyproject.toml index 42566e0bfd0..1c97d9d0220 100644 --- a/libs/partners/anthropic/pyproject.toml +++ b/libs/partners/anthropic/pyproject.toml @@ -60,8 +60,58 @@ plugins = ['pydantic.mypy'] target-version = "py39" [tool.ruff.lint] -select = ["E", "F", "I", "T201", "UP", "S"] -ignore = [ "UP007", ] +select = [ + "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] omit = ["tests/*"] @@ -78,4 +128,5 @@ asyncio_mode = "auto" "tests/**/*.py" = [ "S101", # Tests need assertions "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "SLF001", # Private member access in tests ] diff --git a/libs/partners/anthropic/tests/conftest.py b/libs/partners/anthropic/tests/conftest.py index 8a72f4ba99b..d008846ec3c 100644 --- a/libs/partners/anthropic/tests/conftest.py +++ b/libs/partners/anthropic/tests/conftest.py @@ -20,9 +20,7 @@ def remove_response_headers(response: dict) -> dict: @pytest.fixture(scope="session") 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["before_record_request"] = remove_request_headers config["before_record_response"] = remove_response_headers diff --git a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py index a9e6a954232..d5ea341cb5a 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py @@ -1,5 +1,7 @@ """Test ChatAnthropic chat model.""" +from __future__ import annotations + import asyncio import json import os @@ -43,10 +45,7 @@ def test_stream() -> None: chunks_with_model_name = 0 for token in llm.stream("I'm Pickle Rick"): assert isinstance(token.content, str) - if full is None: - full = cast(BaseMessageChunk, token) - else: - full = full + token + full = cast(BaseMessageChunk, token) if full is None else full + token assert isinstance(token, AIMessageChunk) if token.usage_metadata is not None: if token.usage_metadata.get("input_tokens"): @@ -55,11 +54,14 @@ def test_stream() -> None: chunks_with_output_token_counts += 1 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: - raise AssertionError( + msg = ( "Expected exactly one chunk with input or output token counts. " "AIMessageChunk aggregation adds counts. Check that " "this is behaving properly." ) + raise AssertionError( + msg, + ) assert chunks_with_model_name == 1 # check token usage is populated assert isinstance(full, AIMessageChunk) @@ -85,10 +87,7 @@ async def test_astream() -> None: chunks_with_output_token_counts = 0 async for token in llm.astream("I'm Pickle Rick"): assert isinstance(token.content, str) - if full is None: - full = cast(BaseMessageChunk, token) - else: - full = full + token + full = cast(BaseMessageChunk, token) if full is None else full + token assert isinstance(token, AIMessageChunk) if token.usage_metadata is not None: if token.usage_metadata.get("input_tokens"): @@ -96,11 +95,14 @@ async def test_astream() -> None: if token.usage_metadata.get("output_tokens"): 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. " "AIMessageChunk aggregation adds counts. Check that " "this is behaving properly." ) + raise AssertionError( + msg, + ) # check token usage is populated assert isinstance(full, AIMessageChunk) 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] 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: assert isinstance(token.content, str) @@ -187,8 +190,8 @@ async def test_async_tool_use() -> None: "type": "object", "properties": {"location": {"type": "string"}}, }, - } - ] + }, + ], ) response = await llm_with_tools.ainvoke("what's the weather in san francisco, ca") assert isinstance(response, AIMessage) @@ -202,16 +205,16 @@ async def test_async_tool_use() -> None: # Test streaming first = True - chunks = [] # type: ignore + chunks: list[BaseMessage | BaseMessageChunk] = [] 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: gathered = chunk first = False else: - gathered = gathered + chunk # type: ignore + gathered = gathered + chunk # type: ignore[assignment] assert len(chunks) > 1 assert isinstance(gathered, AIMessageChunk) assert isinstance(gathered.tool_call_chunks, list) @@ -244,12 +247,12 @@ def test_invoke() -> None: """Test invoke tokens from ChatAnthropicMessages.""" 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) 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] prompt = ChatPromptTemplate.from_messages( @@ -260,7 +263,7 @@ def test_system_invoke() -> None: "STAY IN CHARACTER", ), ("human", "Are you a mathematician?"), - ] + ], ) chain = prompt | llm @@ -282,7 +285,7 @@ def test_anthropic_generate() -> None: """Test generate method of anthropic.""" chat = ChatAnthropic(model=MODEL_NAME) 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] result: LLMResult = chat.generate(chat_messages) @@ -330,7 +333,7 @@ async def test_anthropic_async_streaming_callback() -> None: verbose=True, ) 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): assert isinstance(token, AIMessageChunk) @@ -352,8 +355,8 @@ def test_anthropic_multimodal() -> None: }, }, {"type": "text", "text": "What is this a logo for?"}, - ] - ) + ], + ), ] response = chat.invoke(messages) assert isinstance(response, AIMessage) @@ -368,7 +371,9 @@ def test_streaming() -> None: callback_manager = CallbackManager([callback_handler]) 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")]]) @@ -382,7 +387,9 @@ async def test_astreaming() -> None: callback_manager = CallbackManager([callback_handler]) 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")]]) @@ -421,19 +428,19 @@ def test_tool_use() -> None: temperature=0, # Add extra headers to also test token-efficient tools 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]) first = True - chunks = [] # type: ignore + chunks: list[BaseMessage | BaseMessageChunk] = [] for chunk in llm_with_tools.stream(query): - chunks = chunks + [chunk] + chunks = [*chunks, chunk] if first: gathered = chunk first = False else: - gathered = gathered + chunk # type: ignore + gathered = gathered + chunk # type: ignore[assignment] assert len(chunks) > 1 assert isinstance(gathered.content, list) assert len(gathered.content) == 2 @@ -470,17 +477,17 @@ def test_tool_use() -> None: query, gathered, ToolMessage(content="sunny and warm", tool_call_id=tool_call["id"]), - ] + ], ) - chunks = [] # type: ignore + chunks = [] first = True for chunk in stream: - chunks = chunks + [chunk] + chunks = [*chunks, chunk] if first: gathered = chunk first = False else: - gathered = gathered + chunk # type: ignore + gathered = gathered + chunk # type: ignore[assignment] assert len(chunks) > 1 @@ -489,14 +496,14 @@ def test_builtin_tools() -> None: tool = {"type": "text_editor_20250124", "name": "str_replace_editor"} llm_with_tools = llm.bind_tools([tool]) 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 response.tool_calls 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 hair_color: str @@ -508,7 +515,7 @@ def test_disable_parallel_tool_calling() -> None: result = llm_with_tools.invoke( "Use the GenerateUsername tool to generate user names for:\n\n" "Sally with green hair\n" - "Bob with blue hair" + "Bob with blue hair", ) assert isinstance(result, AIMessage) assert len(result.tool_calls) == 1 @@ -523,7 +530,7 @@ def test_anthropic_with_empty_text_block() -> None: return "OK" model = ChatAnthropic(model="claude-3-opus-20240229", temperature=0).bind_tools( - [type_letter] + [type_letter], ) 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 " "anything else or provide any explanations. For example, " "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"), AIMessage( @@ -572,7 +579,7 @@ def test_with_structured_output() -> None: "type": "object", "properties": {"location": {"type": "string"}}, }, - } + }, ) response = structured_llm.invoke("what's the weather in san francisco, ca") assert isinstance(response, dict) @@ -593,10 +600,11 @@ def test_get_num_tokens_from_messages() -> None: # Test tool use @tool(parse_docstring=True) def get_weather(location: str) -> str: - """Get the current weather in a given location + """Get the current weather in a given location. Args: location: The city and state, e.g. San Francisco, CA + """ return "Sunny" @@ -634,7 +642,7 @@ def test_get_num_tokens_from_messages() -> None: 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") @@ -666,9 +674,9 @@ def test_pdf_document_input() -> None: "media_type": "application/pdf", }, }, - ] - ) - ] + ], + ), + ], ) assert isinstance(result, AIMessage) assert isinstance(result.content, str) @@ -694,7 +702,7 @@ def test_citations() -> None: }, {"type": "text", "text": "What color is the grass and sky?"}, ], - } + }, ] response = llm.invoke(messages) assert isinstance(response, AIMessage) @@ -704,10 +712,7 @@ def test_citations() -> None: # Test streaming full: Optional[BaseMessageChunk] = None for chunk in llm.stream(messages): - if full is None: - full = cast(BaseMessageChunk, chunk) - else: - full = full + chunk + full = cast(BaseMessageChunk, chunk) if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) assert any("citations" in block for block in full.content) @@ -718,7 +723,7 @@ def test_citations() -> None: "role": "user", "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 @@ -742,10 +747,7 @@ def test_thinking() -> None: # Test streaming full: Optional[BaseMessageChunk] = None for chunk in llm.stream([input_message]): - if full is None: - full = cast(BaseMessageChunk, chunk) - else: - full = full + chunk + full = cast(BaseMessageChunk, chunk) if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) assert any("thinking" in block for block in full.content) @@ -784,10 +786,7 @@ def test_redacted_thinking() -> None: # Test streaming full: Optional[BaseMessageChunk] = None for chunk in llm.stream([input_message]): - if full is None: - full = cast(BaseMessageChunk, chunk) - else: - full = full + chunk + full = cast(BaseMessageChunk, chunk) if full is None else full + chunk assert isinstance(full, AIMessageChunk) assert isinstance(full.content, list) stream_has_reasoning = False @@ -864,7 +863,7 @@ def test_image_tool_calling() -> None: "media_type": "image/jpeg", "data": image_data, }, - } + }, ) messages = [ SystemMessage("you're a good assistant"), @@ -878,7 +877,7 @@ def test_image_tool_calling() -> None: "id": "foo", "name": "color_picker", }, - ] + ], ), HumanMessage( [ @@ -889,12 +888,12 @@ def test_image_tool_calling() -> None: { "type": "text", "text": "green is a great pick! that's my sister's favorite color", # noqa: E501 - } + }, ], "is_error": False, }, {"type": "text", "text": "what's my sister's favorite color"}, - ] + ], ), ] llm = ChatAnthropic(model="claude-3-5-sonnet-latest") @@ -914,7 +913,7 @@ def test_web_search() -> None: { "type": "text", "text": "How do I update a web app to TypeScript 5.5?", - } + }, ], } response = llm_with_tools.invoke([input_message]) @@ -962,7 +961,7 @@ def test_code_execution() -> None: "Calculate the mean and standard deviation of " "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" ), - } + }, ], } response = llm_with_tools.invoke([input_message]) @@ -999,7 +998,7 @@ def test_remote_mcp() -> None: "name": "deepwiki", "tool_configuration": {"enabled": True, "allowed_tools": ["ask_question"]}, "authorization_token": "PLACEHOLDER", - } + }, ] llm = ChatAnthropic( @@ -1018,7 +1017,7 @@ def test_remote_mcp() -> None: "What transport protocols does the 2025-03-26 version of the MCP " "spec (modelcontextprotocol/modelcontextprotocol) support?" ), - } + }, ], } 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 " "through the HR portal. Approval will be sent by email." ), - } + }, ], - } + }, ] tool_call = { @@ -1182,7 +1181,7 @@ def test_search_result_top_level() -> None: "To request vacation days, submit a leave request form " "through the HR portal. Approval will be sent by email." ), - } + }, ], }, { @@ -1194,14 +1193,14 @@ def test_search_result_top_level() -> None: { "type": "text", "text": "Managers have 3 days to approve a request.", - } + }, ], }, { "type": "text", "text": "How do I request vacation days?", }, - ] + ], ) result = llm.invoke([input_message]) assert isinstance(result, AIMessage) diff --git a/libs/partners/anthropic/tests/integration_tests/test_compile.py b/libs/partners/anthropic/tests/integration_tests/test_compile.py index 33ecccdfa0f..f315e45f521 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_compile.py +++ b/libs/partners/anthropic/tests/integration_tests/test_compile.py @@ -4,4 +4,3 @@ import pytest @pytest.mark.compile def test_placeholder() -> None: """Used for compiling integration tests without running any real tests.""" - pass diff --git a/libs/partners/anthropic/tests/integration_tests/test_experimental.py b/libs/partners/anthropic/tests/integration_tests/test_experimental.py index 67f5cd23ae8..beca1150eb4 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_experimental.py +++ b/libs/partners/anthropic/tests/integration_tests/test_experimental.py @@ -1,5 +1,7 @@ """Test ChatAnthropic chat model.""" +from __future__ import annotations + from enum import Enum 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] 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: assert isinstance(token.content, str) @@ -73,12 +76,12 @@ def test_invoke() -> None: """Test invoke tokens from ChatAnthropicTools.""" llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] - result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"])) + result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]}) assert isinstance(result.content, str) def test_system_invoke() -> None: - """Test invoke tokens with a system message""" + """Test invoke tokens with a system message.""" llm = ChatAnthropicTools(model_name=MODEL_NAME) # type: ignore[call-arg, call-arg] prompt = ChatPromptTemplate.from_messages( @@ -89,7 +92,7 @@ def test_system_invoke() -> None: "STAY IN CHARACTER", ), ("human", "Are you a mathematician?"), - ] + ], ) chain = prompt | llm @@ -128,19 +131,24 @@ def test_anthropic_complex_structured_output() -> None: """Relevant information about an email.""" 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( - None, description="The sender's phone number, if available" + None, + description="The sender's phone number, if available", ) 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( - ..., description="A list of action items requested by the email" + ..., + description="A list of action items requested by the email", ) 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.") @@ -150,7 +158,7 @@ def test_anthropic_complex_structured_output() -> None: "human", "What can you tell me about the following email? Make sure to answer in the correct format: {email}", # noqa: E501 ), - ] + ], ) llm = ChatAnthropicTools( # type: ignore[call-arg, call-arg] @@ -163,7 +171,7 @@ def test_anthropic_complex_structured_output() -> None: 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) diff --git a/libs/partners/anthropic/tests/integration_tests/test_standard.py b/libs/partners/anthropic/tests/integration_tests/test_standard.py index 6f4e3f4f986..686a97c1bd4 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_standard.py +++ b/libs/partners/anthropic/tests/integration_tests/test_standard.py @@ -1,4 +1,4 @@ -"""Standard LangChain interface tests""" +"""Standard LangChain interface tests.""" from pathlib import Path from typing import Literal, cast @@ -87,9 +87,9 @@ class TestAnthropicStandard(ChatModelIntegrationTests): "type": "text", "text": input_, "cache_control": {"type": "ephemeral"}, - } + }, ], - } + }, ], stream, ) @@ -118,9 +118,9 @@ class TestAnthropicStandard(ChatModelIntegrationTests): "type": "text", "text": input_, "cache_control": {"type": "ephemeral"}, - } + }, ], - } + }, ], stream, ) @@ -134,22 +134,18 @@ class TestAnthropicStandard(ChatModelIntegrationTests): "type": "text", "text": input_, "cache_control": {"type": "ephemeral"}, - } + }, ], - } + }, ], stream, ) -def _invoke(llm: ChatAnthropic, input_: list, stream: bool) -> AIMessage: +def _invoke(llm: ChatAnthropic, input_: list, stream: bool) -> AIMessage: # noqa: FBT001 if stream: full = None for chunk in llm.stream(input_): - if full is None: - full = cast(BaseMessageChunk, chunk) - else: - full = full + chunk + full = cast(BaseMessageChunk, chunk) if full is None else full + chunk return cast(AIMessage, full) - else: - return cast(AIMessage, llm.invoke(input_)) + return cast(AIMessage, llm.invoke(input_)) diff --git a/libs/partners/anthropic/tests/unit_tests/_utils.py b/libs/partners/anthropic/tests/unit_tests/_utils.py index 2d10ef80f51..5f0db1f9f41 100644 --- a/libs/partners/anthropic/tests/unit_tests/_utils.py +++ b/libs/partners/anthropic/tests/unit_tests/_utils.py @@ -1,5 +1,7 @@ """A fake callback handler for testing purposes.""" +from __future__ import annotations + from typing import Any, Union from langchain_core.callbacks import BaseCallbackHandler @@ -252,5 +254,5 @@ class FakeCallbackHandler(BaseCallbackHandler, BaseFakeCallbackHandlerMixin): self.on_retriever_error_common() # 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 diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index 2ca6b6cdf1f..7663c65083b 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -1,5 +1,7 @@ """Test chat model integration.""" +from __future__ import annotations + import os from typing import Any, Callable, Literal, Optional, cast from unittest.mock import MagicMock, patch @@ -187,7 +189,7 @@ def test__merge_messages() -> None: "text": None, "name": "blah", }, - ] + ], ), ToolMessage("buz output", tool_call_id="1", status="error"), # type: ignore[misc] ToolMessage( @@ -234,7 +236,7 @@ def test__merge_messages() -> None: "text": None, "name": "blah", }, - ] + ], ), HumanMessage( # type: ignore[misc] [ @@ -266,7 +268,7 @@ def test__merge_messages() -> None: "is_error": False, }, {"type": "text", "text": "next thing"}, - ] + ], ), ] actual = _merge_messages(messages) @@ -277,7 +279,7 @@ def test__merge_messages() -> None: ToolMessage("buz output", tool_call_id="1"), # type: ignore[misc] ToolMessage( # type: ignore[misc] 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", ), @@ -292,8 +294,8 @@ def test__merge_messages() -> None: "is_error": False, }, {"type": "tool_result", "content": "blah output", "tool_use_id": "2"}, - ] - ) + ], + ), ] actual = _merge_messages(messages) assert expected == actual @@ -310,7 +312,7 @@ def test__merge_messages_mutation() -> None: ] expected = [ 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) @@ -327,7 +329,7 @@ def test__format_image() -> None: @pytest.fixture() def pydantic() -> type[BaseModel]: class dummy_function(BaseModel): - """dummy function""" + """Dummy function.""" arg1: int = Field(..., description="foo") arg2: Literal["bar", "baz"] = Field(..., description="one of 'bar', 'baz'") @@ -338,13 +340,14 @@ def pydantic() -> type[BaseModel]: @pytest.fixture() def function() -> Callable: def dummy_function(arg1: int, arg2: Literal["bar", "baz"]) -> None: - """dummy function + """Dummy function. Args: + ---- arg1: foo arg2: one of 'bar', 'baz' - """ - pass + + """ # noqa: D401 return dummy_function @@ -358,7 +361,7 @@ def dummy_tool() -> BaseTool: class DummyFunction(BaseTool): # type: ignore[override] args_schema: type[BaseModel] = Schema name: str = "dummy_function" - description: str = "dummy function" + description: str = "Dummy function." def _run(self, *args: Any, **kwargs: Any) -> Any: pass @@ -370,7 +373,7 @@ def dummy_tool() -> BaseTool: def json_schema() -> dict: return { "title": "dummy_function", - "description": "dummy function", + "description": "Dummy function.", "type": "object", "properties": { "arg1": {"description": "foo", "type": "integer"}, @@ -388,7 +391,7 @@ def json_schema() -> dict: def openai_function() -> dict: return { "name": "dummy_function", - "description": "dummy function", + "description": "Dummy function.", "parameters": { "type": "object", "properties": { @@ -413,7 +416,7 @@ def test_convert_to_anthropic_tool( ) -> None: expected = { "name": "dummy_function", - "description": "dummy function", + "description": "Dummy function.", "input_schema": { "type": "object", "properties": { @@ -429,7 +432,7 @@ def test_convert_to_anthropic_tool( } 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 @@ -461,7 +464,7 @@ def test__format_messages_with_tool_calls() -> None: "type": "base64", "media_type": "image/jpeg", }, - } + }, ], tool_call_id="3", ) @@ -478,7 +481,7 @@ def test__format_messages_with_tool_calls() -> None: "name": "bar", "id": "1", "input": {"baz": "buzz"}, - } + }, ], }, { @@ -489,7 +492,7 @@ def test__format_messages_with_tool_calls() -> None: "content": "blurb", "tool_use_id": "1", "is_error": False, - } + }, ], }, { @@ -500,7 +503,7 @@ def test__format_messages_with_tool_calls() -> None: "name": "bar", "id": "2", "input": {"baz": "buzz"}, - } + }, ], }, { @@ -516,7 +519,7 @@ def test__format_messages_with_tool_calls() -> None: "type": "base64", "media_type": "image/jpeg", }, - } + }, ], "tool_use_id": "2", "is_error": False, @@ -531,7 +534,7 @@ def test__format_messages_with_tool_calls() -> None: "type": "base64", "media_type": "image/jpeg", }, - } + }, ], "tool_use_id": "3", "is_error": False, @@ -579,7 +582,7 @@ def test__format_messages_with_str_content_and_tool_calls() -> None: "content": "blurb", "tool_use_id": "1", "is_error": False, - } + }, ], }, ], @@ -624,7 +627,7 @@ def test__format_messages_with_list_content_and_tool_calls() -> None: "content": "blurb", "tool_use_id": "1", "is_error": False, - } + }, ], }, ], @@ -676,7 +679,7 @@ def test__format_messages_with_tool_use_blocks_and_tool_calls() -> None: "content": "blurb", "tool_use_id": "1", "is_error": False, - } + }, ], }, ], @@ -690,7 +693,7 @@ def test__format_messages_with_cache_control() -> None: SystemMessage( [ {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}}, - ] + ], ), HumanMessage( [ @@ -699,11 +702,11 @@ def test__format_messages_with_cache_control() -> None: "type": "text", "text": "foo", }, - ] + ], ), ] expected_system = [ - {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}} + {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}}, ] 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"}, ], - } + }, ] actual_system, actual_messages = _format_messages(messages) assert expected_system == actual_system @@ -733,8 +736,8 @@ def test__format_messages_with_cache_control() -> None: "data": "", "cache_control": {"type": "ephemeral"}, }, - ] - ) + ], + ), ] actual_system, actual_messages = _format_messages(messages) assert actual_system is None @@ -756,7 +759,7 @@ def test__format_messages_with_cache_control() -> None: "cache_control": {"type": "ephemeral"}, }, ], - } + }, ] assert actual_messages == expected_messages @@ -773,8 +776,8 @@ def test__format_messages_with_citations() -> None: "citations": {"enabled": True}, }, {"type": "text", "text": "What color is the grass and sky?"}, - ] - ) + ], + ), ] expected_messages = [ { @@ -791,7 +794,7 @@ def test__format_messages_with_citations() -> None: }, {"type": "text", "text": "What color is the grass and sky?"}, ], - } + }, ] actual_system, actual_messages = _format_messages(input_messages) assert actual_system is None @@ -843,7 +846,7 @@ def test__format_messages_openai_image_format() -> None: }, }, ], - } + }, ] assert actual_messages == expected_messages @@ -856,7 +859,7 @@ def test__format_messages_with_multiple_system() -> None: SystemMessage( [ {"type": "text", "text": "foo", "cache_control": {"type": "ephemeral"}}, - ] + ], ), ] 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( - monkeypatch: MonkeyPatch, capsys: CaptureFixture + monkeypatch: MonkeyPatch, + capsys: CaptureFixture, ) -> None: """Test that the API key is masked when passed from an environment variable.""" 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): - """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") @@ -931,14 +935,16 @@ def test_anthropic_bind_tools_tool_choice() -> None: anthropic_api_key="secret-api-key", ) 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"] == { "type": "tool", "name": "GetWeather", } 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"] == { "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") 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") 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: """Persist a run.""" - pass def on_chat_model_start(self, *args: Any, **kwargs: Any) -> Run: self.chat_model_start_inputs.append({"args": args, "kwargs": kwargs}) @@ -1036,7 +1041,7 @@ def test_mcp_tracing() -> None: "url": "https://mcp.deepwiki.com/mcp", "name": "deepwiki", "authorization_token": "PLACEHOLDER", - } + }, ] llm = ChatAnthropic( diff --git a/libs/partners/anthropic/tests/unit_tests/test_output_parsers.py b/libs/partners/anthropic/tests/unit_tests/test_output_parsers.py index 7bbb517c00c..988b6ff2634 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_output_parsers.py +++ b/libs/partners/anthropic/tests/unit_tests/test_output_parsers.py @@ -95,7 +95,8 @@ def test_tools_output_parser_empty_content() -> None: chart_type: Literal["pie", "line", "bar"] output_parser = ToolsOutputParser( - first_tool_only=True, pydantic_schemas=[ChartType] + first_tool_only=True, + pydantic_schemas=[ChartType], ) message = AIMessage( "", @@ -105,7 +106,7 @@ def test_tools_output_parser_empty_content() -> None: "args": {"chart_type": "pie"}, "id": "foo", "type": "tool_call", - } + }, ], ) actual = output_parser.invoke(message) diff --git a/libs/partners/anthropic/tests/unit_tests/test_standard.py b/libs/partners/anthropic/tests/unit_tests/test_standard.py index 78546afef9c..832dff42bf2 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_standard.py +++ b/libs/partners/anthropic/tests/unit_tests/test_standard.py @@ -1,4 +1,4 @@ -"""Standard LangChain interface tests""" +"""Standard LangChain interface tests.""" import pytest from langchain_core.language_models import BaseChatModel diff --git a/libs/partners/anthropic/uv.lock b/libs/partners/anthropic/uv.lock index c34594318ee..445e1f60047 100644 --- a/libs/partners/anthropic/uv.lock +++ b/libs/partners/anthropic/uv.lock @@ -477,7 +477,7 @@ requires-dist = [ [package.metadata.requires-dev] codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }] dev = [{ name = "langchain-core", editable = "../../core" }] -lint = [{ name = "ruff", specifier = ">=0.5,<1.0" }] +lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }] test = [ { name = "defusedxml", specifier = ">=0.7.1,<1.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 = "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 = [ { name = "blockbuster", specifier = "~=1.5.18" }, { name = "freezegun", specifier = ">=1.2.2,<2.0.0" }, @@ -598,7 +598,7 @@ requires-dist = [ [package.metadata.requires-dev] 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-integration = [] typing = [ @@ -1513,27 +1513,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.5.7" +version = "0.12.2" 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 = [ - { 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]]