diff --git a/docs/api_reference/create_api_rst.py b/docs/api_reference/create_api_rst.py index dcab7969504..12b42d00156 100644 --- a/docs/api_reference/create_api_rst.py +++ b/docs/api_reference/create_api_rst.py @@ -214,7 +214,7 @@ def _load_package_modules( Traversal based on the file system makes it easy to determine which of the modules/packages are part of the package vs. 3rd party or built-in. - Parameters: + Args: package_directory: Path to the package directory. submodule: Optional name of submodule to load. diff --git a/libs/cli/pyproject.toml b/libs/cli/pyproject.toml index 7b28ba0fed7..3e1fd518864 100644 --- a/libs/cli/pyproject.toml +++ b/libs/cli/pyproject.toml @@ -70,11 +70,14 @@ flake8-annotations.allow-star-arg-any = true flake8-annotations.mypy-init-return = true flake8-type-checking.runtime-evaluated-base-classes = ["pydantic.BaseModel","langchain_core.load.serializable.Serializable","langchain_core.runnables.base.RunnableSerializable"] pep8-naming.classmethod-decorators = [ "classmethod", "langchain_core.utils.pydantic.pre_init", "pydantic.field_validator", "pydantic.v1.root_validator",] -pydocstyle.convention = "google" pyupgrade.keep-runtime-typing = true +[tool.ruff.lint.pydocstyle] +convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters + [tool.ruff.lint.per-file-ignores] -"tests/**" = [ "D1", "DOC", "S", "SLF",] +"tests/**" = [ "D1", "S", "SLF",] "scripts/**" = [ "INP", "S",] [tool.pytest.ini_options] diff --git a/libs/cli/scripts/__init__.py b/libs/cli/scripts/__init__.py deleted file mode 100644 index a9a18d78f25..00000000000 --- a/libs/cli/scripts/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Scripts.""" diff --git a/libs/core/langchain_core/_api/beta_decorator.py b/libs/core/langchain_core/_api/beta_decorator.py index 7326665289f..c0064ac21cd 100644 --- a/libs/core/langchain_core/_api/beta_decorator.py +++ b/libs/core/langchain_core/_api/beta_decorator.py @@ -227,17 +227,17 @@ def warn_beta( ) -> None: """Display a standardized beta annotation. - Arguments: - message : str, optional + Args: + message: Override the default beta message. The %(name)s, %(obj_type)s, %(addendum)s format specifiers will be replaced by the values of the respective arguments passed to this function. - name : str, optional + name: The name of the annotated object. - obj_type : str, optional + obj_type: The object type being annotated. - addendum : str, optional + addendum: Additional text appended directly to the final message. """ if not message: diff --git a/libs/core/langchain_core/_api/deprecation.py b/libs/core/langchain_core/_api/deprecation.py index bac037fbce5..104906eb8f0 100644 --- a/libs/core/langchain_core/_api/deprecation.py +++ b/libs/core/langchain_core/_api/deprecation.py @@ -431,35 +431,35 @@ def warn_deprecated( ) -> None: """Display a standardized deprecation. - Arguments: - since : str + Args: + since: The release at which this API became deprecated. - message : str, optional + message: Override the default deprecation message. The %(since)s, %(name)s, %(alternative)s, %(obj_type)s, %(addendum)s, and %(removal)s format specifiers will be replaced by the values of the respective arguments passed to this function. - name : str, optional + name: The name of the deprecated object. - alternative : str, optional + alternative: An alternative API that the user may use in place of the deprecated API. The deprecation warning will tell the user about this alternative if provided. - alternative_import: str, optional + alternative_import: An alternative import that the user may use instead. - pending : bool, optional + pending: If True, uses a PendingDeprecationWarning instead of a DeprecationWarning. Cannot be used together with removal. - obj_type : str, optional + obj_type: The object type being deprecated. - addendum : str, optional + addendum: Additional text appended directly to the final message. - removal : str, optional + removal: The expected removal version. With the default (an empty string), a removal version is automatically computed from since. Set to other Falsy values to not schedule a removal date. Cannot be used together with pending. - package: str, optional + package: The package of the deprecated object. """ if not pending: diff --git a/libs/core/langchain_core/load/serializable.py b/libs/core/langchain_core/load/serializable.py index d1523bfed53..9196416a71c 100644 --- a/libs/core/langchain_core/load/serializable.py +++ b/libs/core/langchain_core/load/serializable.py @@ -111,7 +111,7 @@ class Serializable(BaseModel, ABC): # Remove default BaseModel init docstring. def __init__(self, *args: Any, **kwargs: Any) -> None: - """""" # noqa: D419 + """""" # noqa: D419 # Intentional blank docstring super().__init__(*args, **kwargs) @classmethod diff --git a/libs/core/langchain_core/tools/convert.py b/libs/core/langchain_core/tools/convert.py index 0c48b3c7e4d..585a6e19528 100644 --- a/libs/core/langchain_core/tools/convert.py +++ b/libs/core/langchain_core/tools/convert.py @@ -227,7 +227,7 @@ def tool( \"\"\" return bar - """ # noqa: D214, D410, D411 + """ # noqa: D214, D410, D411 # We're intentionally showing bad formatting in examples def _create_tool_factory( tool_name: str, diff --git a/libs/core/langchain_core/utils/function_calling.py b/libs/core/langchain_core/utils/function_calling.py index b36db207ed6..647afb59812 100644 --- a/libs/core/langchain_core/utils/function_calling.py +++ b/libs/core/langchain_core/utils/function_calling.py @@ -667,14 +667,13 @@ def tool_example_to_messages( The ``ToolMessage`` is required because some chat models are hyper-optimized for agents rather than for an extraction use case. - Arguments: - input: string, the user input - tool_calls: list[BaseModel], a list of tool calls represented as Pydantic - BaseModels - tool_outputs: Optional[list[str]], a list of tool call outputs. + Args: + input: The user input + tool_calls: Tool calls represented as Pydantic BaseModels + tool_outputs: Tool call outputs. Does not need to be provided. If not provided, a placeholder value will be inserted. Defaults to None. - ai_response: Optional[str], if provided, content for a final ``AIMessage``. + ai_response: If provided, content for a final ``AIMessage``. Returns: A list of messages diff --git a/libs/core/pyproject.toml b/libs/core/pyproject.toml index 5e92bbe6a70..d915c917b89 100644 --- a/libs/core/pyproject.toml +++ b/libs/core/pyproject.toml @@ -99,7 +99,6 @@ ignore = [ # TODO rules "ANN401", # No Any types "BLE", # Blind exceptions - "DOC", # Docstrings (preview) "ERA", # No commented-out code "PLR2004", # Comparison to magic number ] @@ -113,10 +112,12 @@ flake8-annotations.mypy-init-return = true flake8-builtins.ignorelist = ["id", "input", "type"] flake8-type-checking.runtime-evaluated-base-classes = ["pydantic.BaseModel","langchain_core.load.serializable.Serializable","langchain_core.runnables.base.RunnableSerializable"] pep8-naming.classmethod-decorators = [ "classmethod", "langchain_core.utils.pydantic.pre_init", "pydantic.field_validator", "pydantic.v1.root_validator",] -pydocstyle.convention = "google" -pydocstyle.ignore-var-parameters = true pyupgrade.keep-runtime-typing = true +[tool.ruff.lint.pydocstyle] +convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters + [tool.ruff.lint.per-file-ignores] "langchain_core/utils/mustache.py" = [ "PLW0603",] "tests/unit_tests/test_tools.py" = [ "ARG",] diff --git a/libs/core/tests/unit_tests/test_tools.py b/libs/core/tests/unit_tests/test_tools.py index ba328442013..72c7c8517c5 100644 --- a/libs/core/tests/unit_tests/test_tools.py +++ b/libs/core/tests/unit_tests/test_tools.py @@ -1344,7 +1344,7 @@ def test_tool_invalid_docstrings() -> None: Args: bar: The bar. baz: The baz. - """ # noqa: D205,D411 + """ # noqa: D205,D411 # We're intentionally testing bad formatting. return bar for func in {foo3, foo4}: diff --git a/libs/langchain/langchain/agents/mrkl/base.py b/libs/langchain/langchain/agents/mrkl/base.py index f5af05a4234..115a201a0db 100644 --- a/libs/langchain/langchain/agents/mrkl/base.py +++ b/libs/langchain/langchain/agents/mrkl/base.py @@ -26,7 +26,7 @@ from langchain.chains import LLMChain class ChainConfig(NamedTuple): """Configuration for a chain to use in MRKL system. - Parameters: + Args: action_name: Name of the action. action: Action function to call. action_description: Description of the action. @@ -45,7 +45,7 @@ class ChainConfig(NamedTuple): class ZeroShotAgent(Agent): """Agent for the MRKL chain. - Parameters: + Args: output_parser: Output parser for the agent. """ diff --git a/libs/langchain/langchain/agents/openai_assistant/base.py b/libs/langchain/langchain/agents/openai_assistant/base.py index 76504be2bb1..71dc19abbb7 100644 --- a/libs/langchain/langchain/agents/openai_assistant/base.py +++ b/libs/langchain/langchain/agents/openai_assistant/base.py @@ -33,7 +33,7 @@ if TYPE_CHECKING: class OpenAIAssistantFinish(AgentFinish): """AgentFinish with run and thread metadata. - Parameters: + Args: run_id: Run id. thread_id: Thread id. """ @@ -54,7 +54,7 @@ class OpenAIAssistantFinish(AgentFinish): class OpenAIAssistantAction(AgentAction): """AgentAction with info needed to submit custom tool output to existing run. - Parameters: + Args: tool_call_id: Tool call id. run_id: Run id. thread_id: Thread id @@ -322,7 +322,7 @@ class OpenAIAssistantRunnable(RunnableSerializable[dict, OutputType]): config: Runnable config. Defaults to None. **kwargs: Additional arguments. - Return: + Returns: If self.as_agent, will return Union[List[OpenAIAssistantAction], OpenAIAssistantFinish]. Otherwise, will return OpenAI types @@ -456,7 +456,7 @@ class OpenAIAssistantRunnable(RunnableSerializable[dict, OutputType]): config: Runnable config. Defaults to None. kwargs: Additional arguments. - Return: + Returns: If self.as_agent, will return Union[List[OpenAIAssistantAction], OpenAIAssistantFinish]. Otherwise, will return OpenAI types diff --git a/libs/langchain/langchain/agents/openai_functions_agent/agent_token_buffer_memory.py b/libs/langchain/langchain/agents/openai_functions_agent/agent_token_buffer_memory.py index cc403fd62cc..2dfca17c542 100644 --- a/libs/langchain/langchain/agents/openai_functions_agent/agent_token_buffer_memory.py +++ b/libs/langchain/langchain/agents/openai_functions_agent/agent_token_buffer_memory.py @@ -16,7 +16,7 @@ from langchain.memory.chat_memory import BaseChatMemory class AgentTokenBufferMemory(BaseChatMemory): """Memory used to save agent output AND intermediate steps. - Parameters: + Args: human_prefix: Prefix for human messages. Default is "Human". ai_prefix: Prefix for AI messages. Default is "AI". llm: Language model. diff --git a/libs/langchain/langchain/chains/openai_functions/openapi.py b/libs/langchain/langchain/chains/openai_functions/openapi.py index 210a809ddd4..1b48b87bedb 100644 --- a/libs/langchain/langchain/chains/openai_functions/openapi.py +++ b/libs/langchain/langchain/chains/openai_functions/openapi.py @@ -268,7 +268,7 @@ def get_openapi_chain( params: Optional[dict] = None, **kwargs: Any, ) -> SequentialChain: - """Create a chain for querying an API from a OpenAPI spec. + r"""Create a chain for querying an API from a OpenAPI spec. Note: this class is deprecated. See below for a replacement implementation. The benefits of this implementation are: @@ -359,7 +359,7 @@ def get_openapi_chain( llm_chain_kwargs: LLM chain additional keyword arguments. **kwargs: Additional keyword arguments to pass to the chain. - """ # noqa: E501,D301 + """ # noqa: E501 try: from langchain_community.utilities.openapi import OpenAPISpec except ImportError as e: diff --git a/libs/langchain/langchain/smith/evaluation/runner_utils.py b/libs/langchain/langchain/smith/evaluation/runner_utils.py index 5fc4e8f9805..423eef8a4ca 100644 --- a/libs/langchain/langchain/smith/evaluation/runner_utils.py +++ b/libs/langchain/langchain/smith/evaluation/runner_utils.py @@ -299,7 +299,7 @@ def _get_prompt(inputs: dict[str, Any]) -> str: class ChatModelInput(TypedDict): """Input for a chat model. - Parameters: + Args: messages: List of chat messages. """ diff --git a/libs/langchain/pyproject.toml b/libs/langchain/pyproject.toml index 91bfe593f0d..904b74abfc0 100644 --- a/libs/langchain/pyproject.toml +++ b/libs/langchain/pyproject.toml @@ -159,7 +159,6 @@ ignore = [ # TODO rules "ANN401", # No type Any "D100", # pydocstyle: missing docstring in public module - "D104", # pydocstyle: missing docstring in public package "PLC0415", # pylint: import-outside-top-level "TRY301", # tryceratops: raise-within-try ] @@ -172,9 +171,12 @@ flake8-annotations.allow-star-arg-any = true flake8-annotations.mypy-init-return = true flake8-type-checking.runtime-evaluated-base-classes = ["pydantic.BaseModel","langchain_core.load.serializable.Serializable","langchain_core.runnables.base.RunnableSerializable"] pep8-naming.classmethod-decorators = [ "classmethod", "langchain_core.utils.pre_init", "pydantic.field_validator", "pydantic.v1.root_validator",] -pydocstyle.convention = "google" pyupgrade.keep-runtime-typing = true +[tool.ruff.lint.pydocstyle] +convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters + [tool.ruff.lint.extend-per-file-ignores] "tests/**/*.py" = [ "D1", # Docstrings not mandatory in tests @@ -198,6 +200,9 @@ pyupgrade.keep-runtime-typing = true "DTZ005", # Use of non timezone-aware datetime "DTZ006", # Use of non timezone-aware datetime ] +"**/__init__.py" = [ + "D104", # Missing docstring in public package +] [tool.coverage.run] omit = ["tests/*"] diff --git a/libs/langchain_v1/pyproject.toml b/libs/langchain_v1/pyproject.toml index adf06e8be13..842ef127795 100644 --- a/libs/langchain_v1/pyproject.toml +++ b/libs/langchain_v1/pyproject.toml @@ -113,10 +113,13 @@ ignore = [ ] unfixable = ["B028"] # People should intentionally tune the stacklevel -pydocstyle.convention = "google" pyupgrade.keep-runtime-typing = true flake8-annotations.allow-star-arg-any = true +[tool.ruff.lint.pydocstyle] +convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters + [tool.ruff.lint.per-file-ignores] "tests/*" = [ "D1", # Documentation rules diff --git a/libs/partners/anthropic/langchain_anthropic/__init__.py b/libs/partners/anthropic/langchain_anthropic/__init__.py index 399fece0c2d..774a69cc008 100644 --- a/libs/partners/anthropic/langchain_anthropic/__init__.py +++ b/libs/partners/anthropic/langchain_anthropic/__init__.py @@ -1,3 +1,5 @@ +"""Anthropic partner package for LangChain.""" + from langchain_anthropic.chat_models import ( ChatAnthropic, ChatAnthropicMessages, diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index d650291972e..0ef5043406e 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -1,3 +1,5 @@ +"""Anthropic chat models.""" + from __future__ import annotations import copy @@ -172,12 +174,12 @@ def _merge_messages( all(isinstance(m, c) for m in (curr, last)) for c in (SystemMessage, HumanMessage) ): - if isinstance(cast(BaseMessage, last).content, str): + 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)) + new_content = copy.copy(cast("list", cast("BaseMessage", last).content)) if isinstance(curr.content, str): new_content.append({"type": "text", "text": curr.content}) else: @@ -475,14 +477,14 @@ def _format_messages( else content ) tool_use_ids = [ - cast(dict, block)["id"] + cast("dict", block)["id"] for block in content - if cast(dict, block)["type"] == "tool_use" + if cast("dict", block)["type"] == "tool_use" ] missing_tool_calls = [ 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), ) @@ -1418,6 +1420,7 @@ class ChatAnthropic(BaseChatModel): @property def lc_secrets(self) -> dict[str, str]: + """Return a mapping of secret keys to environment variables.""" return { "anthropic_api_key": "ANTHROPIC_API_KEY", "mcp_servers": "ANTHROPIC_MCP_SERVERS", @@ -1425,6 +1428,7 @@ class ChatAnthropic(BaseChatModel): @classmethod def is_lc_serializable(cls) -> bool: + """Whether the class is serializable in langchain.""" return True @classmethod @@ -1470,6 +1474,7 @@ class ChatAnthropic(BaseChatModel): @model_validator(mode="before") @classmethod def build_extra(cls, values: dict) -> Any: + """Build model kwargs.""" all_required_field_names = get_pydantic_field_names(cls) return _build_model_kwargs(values, all_required_field_names) @@ -2265,7 +2270,7 @@ class ChatAnthropic(BaseChatModel): def get_weather(location: str) -> str: \"\"\"Get the current weather in a given location - Args: + Args: location: The city and state, e.g. San Francisco, CA \"\"\" return "Sunny" @@ -2359,7 +2364,7 @@ def _lc_tool_calls_to_anthropic_tool_use_blocks( type="tool_use", name=tool_call["name"], input=tool_call["args"], - id=cast(str, tool_call["id"]), + id=cast("str", tool_call["id"]), ) for tool_call in tool_calls ] @@ -2469,10 +2474,7 @@ def _make_message_chunk_from_anthropic_event( message_chunk = AIMessageChunk(content=[content_block]) # Reasoning - elif ( - event.delta.type == "thinking_delta" - or event.delta.type == "signature_delta" - ): + elif event.delta.type in {"thinking_delta", "signature_delta"}: content_block = event.delta.model_dump() content_block["index"] = event.index content_block["type"] = "thinking" @@ -2526,7 +2528,7 @@ def _make_message_chunk_from_anthropic_event( @deprecated(since="0.1.0", removal="1.0.0", alternative="ChatAnthropic") class ChatAnthropicMessages(ChatAnthropic): - pass + """Deprecated class, use `ChatAnthropic` instead.""" def _create_usage_metadata(anthropic_usage: BaseModel) -> UsageMetadata: diff --git a/libs/partners/anthropic/langchain_anthropic/experimental.py b/libs/partners/anthropic/langchain_anthropic/experimental.py index ec30ddcf687..37e4f116007 100644 --- a/libs/partners/anthropic/langchain_anthropic/experimental.py +++ b/libs/partners/anthropic/langchain_anthropic/experimental.py @@ -1,3 +1,5 @@ +"""Experimental tool-calling support for Anthropic chat models.""" + from __future__ import annotations import json diff --git a/libs/partners/anthropic/langchain_anthropic/llms.py b/libs/partners/anthropic/langchain_anthropic/llms.py index 85ac112d017..3b628e0bc6f 100644 --- a/libs/partners/anthropic/langchain_anthropic/llms.py +++ b/libs/partners/anthropic/langchain_anthropic/llms.py @@ -1,3 +1,5 @@ +"""Anthropic LLM wrapper. Chat models are in chat_models.py.""" + from __future__ import annotations import re @@ -164,10 +166,12 @@ class AnthropicLLM(LLM, _AnthropicCommon): @property def lc_secrets(self) -> dict[str, str]: + """Return a mapping of secret keys to environment variables.""" return {"anthropic_api_key": "ANTHROPIC_API_KEY"} @classmethod def is_lc_serializable(cls) -> bool: + """Whether this class can be serialized by langchain.""" return True @property @@ -222,10 +226,9 @@ class AnthropicLLM(LLM, _AnthropicCommon): messages.append( {"role": "assistant", "content": assistant_part.strip()} ) - else: - # Just human content - if part.strip(): - messages.append({"role": "user", "content": part.strip()}) + # Just human content + elif part.strip(): + messages.append({"role": "user", "content": part.strip()}) else: # Handle modern format or plain text # Clean prompt for Messages API @@ -291,6 +294,7 @@ class AnthropicLLM(LLM, _AnthropicCommon): return response.content[0].text def convert_prompt(self, prompt: PromptValue) -> str: + """Convert a ``PromptValue`` to a string.""" return prompt.to_string() async def _acall( diff --git a/libs/partners/anthropic/langchain_anthropic/output_parsers.py b/libs/partners/anthropic/langchain_anthropic/output_parsers.py index 83b8b0e8779..7695d020089 100644 --- a/libs/partners/anthropic/langchain_anthropic/output_parsers.py +++ b/libs/partners/anthropic/langchain_anthropic/output_parsers.py @@ -1,3 +1,5 @@ +"""Output parsers for Anthropic tool calls.""" + from __future__ import annotations from typing import Any, Optional, Union, cast @@ -38,7 +40,7 @@ class ToolsOutputParser(BaseGenerationOutputParser): """ if not result or not isinstance(result[0], ChatGeneration): return None if self.first_tool_only else [] - message = cast(AIMessage, result[0].message) + message = cast("AIMessage", result[0].message) tool_calls: list = [ dict(tc) for tc in _extract_tool_calls_from_message(message) ] diff --git a/libs/partners/anthropic/pyproject.toml b/libs/partners/anthropic/pyproject.toml index 7d270ad4c0e..c2dffd98fe9 100644 --- a/libs/partners/anthropic/pyproject.toml +++ b/libs/partners/anthropic/pyproject.toml @@ -63,60 +63,35 @@ docstring-code-format = true docstring-code-line-length = 100 [tool.ruff.lint] -select = [ - "A", # flake8-builtins - "B", # flake8-bugbear - "ASYNC", # flake8-async - "C4", # flake8-comprehensions - "COM", # flake8-commas - "D", # pydocstyle - "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 -] +select = ["ALL"] ignore = [ - "D100", # pydocstyle: Missing docstring in public module - "D101", # pydocstyle: Missing docstring in public class - "D102", # pydocstyle: Missing docstring in public method - "D103", # pydocstyle: Missing docstring in public function - "D104", # pydocstyle: Missing docstring in public package - "D105", # pydocstyle: Missing docstring in magic method - "D107", # pydocstyle: Missing docstring in __init__ - "D407", # pydocstyle: Missing-dashed-underline-after-section - "D203", - "D213", - "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 + "FIX", # TODOs + "TD", # TODOs + "C901", # Complex functions + "PLR0912", # Too many branches + "PLR0913", # Too many arguments + "PLR0914", # Too many local variables + "PLR0915", # Too many statements + "ARG001", + + # TODO + "PLR2004", # Comparison to magic number + "ANN401", + "ARG002", + "BLE001", + "TC", + "PLC0415", + "PT011", + "PT013", + "TRY", + "PLW", + "PLE", ] unfixable = ["B028"] # People should intentionally tune the stacklevel @@ -133,10 +108,15 @@ asyncio_mode = "auto" [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.ruff.lint.extend-per-file-ignores] "tests/**/*.py" = [ "S101", # Tests need assertions "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes "SLF001", # Private member access in tests + "D", # Docstring checks in tests +] +"scripts/*.py" = [ + "INP001", # Not a package ] diff --git a/libs/partners/anthropic/scripts/check_imports.py b/libs/partners/anthropic/scripts/check_imports.py index 58a460c1493..698173fb7bd 100644 --- a/libs/partners/anthropic/scripts/check_imports.py +++ b/libs/partners/anthropic/scripts/check_imports.py @@ -1,3 +1,5 @@ +"""Script to check for import errors in specified Python files.""" + import sys import traceback from importlib.machinery import SourceFileLoader diff --git a/libs/partners/anthropic/tests/conftest.py b/libs/partners/anthropic/tests/conftest.py index d008846ec3c..ba705a7d5d1 100644 --- a/libs/partners/anthropic/tests/conftest.py +++ b/libs/partners/anthropic/tests/conftest.py @@ -2,7 +2,9 @@ from typing import Any import pytest from langchain_tests.conftest import CustomPersister, CustomSerializer -from langchain_tests.conftest import _base_vcr_config as _base_vcr_config +from langchain_tests.conftest import ( + _base_vcr_config as _base_vcr_config, # noqa: PLC0414 +) from vcr import VCR # type: ignore[import-untyped] 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 ea7615fd332..5977ccbdbf7 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py @@ -45,7 +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) - full = cast(BaseMessageChunk, token) if full is None else 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"): @@ -87,7 +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) - full = cast(BaseMessageChunk, token) if full is None else 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"): @@ -711,7 +711,7 @@ def test_citations() -> None: # Test streaming full: Optional[BaseMessageChunk] = None for chunk in llm.stream(messages): - full = cast(BaseMessageChunk, chunk) if full is None else 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) @@ -740,13 +740,15 @@ def test_thinking() -> None: assert isinstance(block, dict) if block["type"] == "thinking": assert set(block.keys()) == {"type", "thinking", "signature"} - assert block["thinking"] and isinstance(block["thinking"], str) - assert block["signature"] and isinstance(block["signature"], str) + assert block["thinking"] + assert isinstance(block["thinking"], str) + assert block["signature"] + assert isinstance(block["signature"], str) # Test streaming full: Optional[BaseMessageChunk] = None for chunk in llm.stream([input_message]): - full = cast(BaseMessageChunk, chunk) if full is None else full + chunk + 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) @@ -754,8 +756,10 @@ def test_thinking() -> None: assert isinstance(block, dict) if block["type"] == "thinking": assert set(block.keys()) == {"type", "thinking", "signature", "index"} - assert block["thinking"] and isinstance(block["thinking"], str) - assert block["signature"] and isinstance(block["signature"], str) + assert block["thinking"] + assert isinstance(block["thinking"], str) + assert block["signature"] + assert isinstance(block["signature"], str) # Test pass back in next_message = {"role": "user", "content": "How are you?"} @@ -779,13 +783,14 @@ def test_redacted_thinking() -> None: if block["type"] == "redacted_thinking": has_reasoning = True assert set(block.keys()) == {"type", "data"} - assert block["data"] and isinstance(block["data"], str) + assert block["data"] + assert isinstance(block["data"], str) assert has_reasoning # Test streaming full: Optional[BaseMessageChunk] = None for chunk in llm.stream([input_message]): - full = cast(BaseMessageChunk, chunk) if full is None else 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 @@ -794,7 +799,8 @@ def test_redacted_thinking() -> None: if block["type"] == "redacted_thinking": stream_has_reasoning = True assert set(block.keys()) == {"type", "data", "index"} - assert block["data"] and isinstance(block["data"], str) + assert block["data"] + assert isinstance(block["data"], str) assert stream_has_reasoning # Test pass back in @@ -841,7 +847,7 @@ def test_structured_output_thinking_force_tool_use() -> None: def test_image_tool_calling() -> None: """Test tool calling with image inputs.""" - class color_picker(BaseModel): + class color_picker(BaseModel): # noqa: N801 """Input your fav color and get a random fact about it.""" fav_color: str diff --git a/libs/partners/anthropic/tests/integration_tests/test_standard.py b/libs/partners/anthropic/tests/integration_tests/test_standard.py index d06eb84cdc9..9443cc44605 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_standard.py +++ b/libs/partners/anthropic/tests/integration_tests/test_standard.py @@ -15,6 +15,8 @@ MODEL = "claude-3-5-haiku-latest" class TestAnthropicStandard(ChatModelIntegrationTests): + """Use the standard ChatModel integration tests against the ChatAnthropic class.""" + @property def chat_model_class(self) -> type[BaseChatModel]: return ChatAnthropic @@ -71,7 +73,7 @@ class TestAnthropicStandard(ChatModelIntegrationTests): llm = ChatAnthropic( model=MODEL, # type: ignore[call-arg] ) - with open(REPO_ROOT_DIR / "README.md") as f: + with Path.open(REPO_ROOT_DIR / "README.md") as f: readme = f.read() input_ = f"""What's langchain? Here's the langchain README: @@ -99,7 +101,7 @@ class TestAnthropicStandard(ChatModelIntegrationTests): llm = ChatAnthropic( model=MODEL, # type: ignore[call-arg] ) - with open(REPO_ROOT_DIR / "README.md") as f: + with Path.open(REPO_ROOT_DIR / "README.md") as f: readme = f.read() input_ = f"""What's langchain? Here's the langchain README: @@ -146,6 +148,6 @@ def _invoke(llm: ChatAnthropic, input_: list, stream: bool) -> AIMessage: # noq if stream: full = None for chunk in llm.stream(input_): - full = cast(BaseMessageChunk, chunk) if full is None else full + chunk - return cast(AIMessage, full) - return cast(AIMessage, llm.invoke(input_)) + full = cast("BaseMessageChunk", chunk) if full is None else full + chunk + return cast("AIMessage", full) + return cast("AIMessage", llm.invoke(input_)) 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 3aad9c4f262..b07c158cc1d 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -41,7 +41,7 @@ def test_initialization() -> None: ), ]: assert model.model == "claude-instant-1.2" - assert cast(SecretStr, model.anthropic_api_key).get_secret_value() == "xyz" + assert cast("SecretStr", model.anthropic_api_key).get_secret_value() == "xyz" assert model.default_request_timeout == 2.0 assert model.anthropic_api_url == "https://api.anthropic.com" @@ -375,9 +375,9 @@ def test__format_image() -> None: _format_image(url) -@pytest.fixture() +@pytest.fixture def pydantic() -> type[BaseModel]: - class dummy_function(BaseModel): + class dummy_function(BaseModel): # noqa: N801 """Dummy function.""" arg1: int = Field(..., description="foo") @@ -386,7 +386,7 @@ def pydantic() -> type[BaseModel]: return dummy_function -@pytest.fixture() +@pytest.fixture def function() -> Callable: def dummy_function(arg1: int, arg2: Literal["bar", "baz"]) -> None: """Dummy function. @@ -400,7 +400,7 @@ def function() -> Callable: return dummy_function -@pytest.fixture() +@pytest.fixture def dummy_tool() -> BaseTool: class Schema(BaseModel): arg1: int = Field(..., description="foo") @@ -417,7 +417,7 @@ def dummy_tool() -> BaseTool: return DummyFunction() -@pytest.fixture() +@pytest.fixture def json_schema() -> dict: return { "title": "dummy_function", @@ -435,7 +435,7 @@ def json_schema() -> dict: } -@pytest.fixture() +@pytest.fixture def openai_function() -> dict: return { "name": "dummy_function", @@ -1007,7 +1007,7 @@ def test_anthropic_uses_actual_secret_value_from_secretstr() -> None: anthropic_api_key="secret-api-key", ) assert ( - cast(SecretStr, chat_model.anthropic_api_key).get_secret_value() + cast("SecretStr", chat_model.anthropic_api_key).get_secret_value() == "secret-api-key" ) @@ -1027,7 +1027,7 @@ def test_anthropic_bind_tools_tool_choice() -> None: [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", "name": "GetWeather", } @@ -1035,16 +1035,16 @@ def test_anthropic_bind_tools_tool_choice() -> None: [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", "name": "GetWeather", } 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", } 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", } @@ -1062,11 +1062,11 @@ def test_get_num_tokens_from_messages_passes_kwargs() -> None: """Test that get_num_tokens_from_messages passes kwargs to the model.""" llm = ChatAnthropic(model="claude-3-5-haiku-latest") - with patch.object(anthropic, "Client") as _Client: + with patch.object(anthropic, "Client") as _client: llm.get_num_tokens_from_messages([HumanMessage("foo")], foo="bar") assert ( - _Client.return_value.beta.messages.count_tokens.call_args.kwargs["foo"] == "bar" + _client.return_value.beta.messages.count_tokens.call_args.kwargs["foo"] == "bar" ) @@ -1110,6 +1110,8 @@ def test_usage_metadata_standardization() -> None: class FakeTracer(BaseTracer): + """Fake tracer to capture inputs to `chat_model_start`.""" + def __init__(self) -> None: super().__init__() self.chat_model_start_inputs: list = [] diff --git a/libs/partners/anthropic/tests/unit_tests/test_standard.py b/libs/partners/anthropic/tests/unit_tests/test_standard.py index 832dff42bf2..d8cdafc159f 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_standard.py +++ b/libs/partners/anthropic/tests/unit_tests/test_standard.py @@ -9,6 +9,8 @@ from langchain_anthropic import ChatAnthropic class TestAnthropicStandard(ChatModelUnitTests): + """Use the standard ChatModel unit tests against the ChatAnthropic class.""" + @property def chat_model_class(self) -> type[BaseChatModel]: return ChatAnthropic diff --git a/libs/partners/chroma/langchain_chroma/vectorstores.py b/libs/partners/chroma/langchain_chroma/vectorstores.py index 98e19c2158d..956e2016aa3 100644 --- a/libs/partners/chroma/langchain_chroma/vectorstores.py +++ b/libs/partners/chroma/langchain_chroma/vectorstores.py @@ -1119,7 +1119,7 @@ class Chroma(VectorStore): Ids are always included. Defaults to `["metadatas", "documents"]`. Optional. - Return: + Returns: A dict with the keys `"ids"`, `"embeddings"`, `"metadatas"`, `"documents"`. """ kwargs = { diff --git a/libs/partners/chroma/pyproject.toml b/libs/partners/chroma/pyproject.toml index 89e84192878..31357a25c75 100644 --- a/libs/partners/chroma/pyproject.toml +++ b/libs/partners/chroma/pyproject.toml @@ -109,7 +109,6 @@ ignore = [ "D104", # pydocstyle: Missing docstring in public package "D105", # pydocstyle: Missing docstring in magic method "D107", # pydocstyle: Missing docstring in __init__ - "D407", # pydocstyle: Missing-dashed-underline-after-section "UP007", # pyupgrade: non-pep604-annotation-union "UP045", # pyupgrade: non-pep604-annotation-optional "COM812", # Messes with the formatter @@ -129,6 +128,7 @@ asyncio_mode = "auto" [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.ruff.lint.per-file-ignores] "tests/**" = ["D"] diff --git a/libs/partners/deepseek/pyproject.toml b/libs/partners/deepseek/pyproject.toml index fe01f538153..19225b6f6e2 100644 --- a/libs/partners/deepseek/pyproject.toml +++ b/libs/partners/deepseek/pyproject.toml @@ -96,8 +96,6 @@ ignore = [ "D104", # Missing docstring in public package "D105", # Missing docstring in magic method "D107", # Missing docstring in __init__ - "D203", # 1 blank line required before class docstring (incompatible with D211) - "D213", # Multi-line docstring summary should start at the second line (incompatible with D212) "UP007", # pyupgrade: non-pep604-annotation-union "UP045", # pyupgrade: non-pep604-annotation-optional ] @@ -115,6 +113,7 @@ asyncio_mode = "auto" [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.ruff.lint.extend-per-file-ignores] "tests/**/*.py" = [ diff --git a/libs/partners/exa/pyproject.toml b/libs/partners/exa/pyproject.toml index b451ac8446e..c4eef03db18 100644 --- a/libs/partners/exa/pyproject.toml +++ b/libs/partners/exa/pyproject.toml @@ -96,8 +96,6 @@ ignore = [ "D104", # Missing docstring in public package "D105", # Missing docstring in magic method "D107", # Missing docstring in __init__ - "D203", # Incompatible with D211 (blank line before class) - "D213", # Incompatible with D212 (multi-line summary) "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful @@ -122,6 +120,7 @@ asyncio_mode = "auto" [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.ruff.lint.extend-per-file-ignores] "tests/**/*.py" = [ diff --git a/libs/partners/fireworks/pyproject.toml b/libs/partners/fireworks/pyproject.toml index d42120d8833..e4b90a32622 100644 --- a/libs/partners/fireworks/pyproject.toml +++ b/libs/partners/fireworks/pyproject.toml @@ -97,8 +97,6 @@ ignore = [ "D104", # Missing docstring in public package "D105", # Missing docstring in magic method "D107", # Missing docstring in __init__ - "D203", - "D213", "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful @@ -112,6 +110,7 @@ unfixable = ["B028"] # People should intentionally tune the stacklevel [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.coverage.run] omit = ["tests/*"] diff --git a/libs/partners/groq/langchain_groq/__init__.py b/libs/partners/groq/langchain_groq/__init__.py index 746b62ad105..47336d474b2 100644 --- a/libs/partners/groq/langchain_groq/__init__.py +++ b/libs/partners/groq/langchain_groq/__init__.py @@ -1,3 +1,5 @@ +"""Groq integration for LangChain.""" + from langchain_groq.chat_models import ChatGroq from langchain_groq.version import __version__ diff --git a/libs/partners/groq/langchain_groq/chat_models.py b/libs/partners/groq/langchain_groq/chat_models.py index ac97ec790a3..15608b4b547 100644 --- a/libs/partners/groq/langchain_groq/chat_models.py +++ b/libs/partners/groq/langchain_groq/chat_models.py @@ -450,7 +450,7 @@ class ChatGroq(BaseChatModel): } try: - import groq + import groq # noqa: PLC0415 sync_specific: dict[str, Any] = {"http_client": self.http_client} if not self.client: @@ -475,6 +475,7 @@ class ChatGroq(BaseChatModel): # @property def lc_secrets(self) -> dict[str, str]: + """Mapping of secret environment variables.""" return {"groq_api_key": "GROQ_API_KEY"} @classmethod @@ -585,7 +586,7 @@ class ChatGroq(BaseChatModel): default_chunk_class: type[BaseMessageChunk] = AIMessageChunk for chunk in self.client.create(messages=message_dicts, **params): if not isinstance(chunk, dict): - chunk = chunk.model_dump() + chunk = chunk.model_dump() # noqa: PLW2901 if len(chunk["choices"]) == 0: continue choice = chunk["choices"][0] @@ -633,7 +634,7 @@ class ChatGroq(BaseChatModel): messages=message_dicts, **params ): if not isinstance(chunk, dict): - chunk = chunk.model_dump() + chunk = chunk.model_dump() # noqa: PLW2901 if len(chunk["choices"]) == 0: continue choice = chunk["choices"][0] @@ -1304,8 +1305,8 @@ def _convert_chunk_to_message_chunk( ) -> BaseMessageChunk: choice = chunk["choices"][0] _dict = choice["delta"] - role = cast(str, _dict.get("role")) - content = cast(str, _dict.get("content") or "") + role = cast("str", _dict.get("role")) + content = cast("str", _dict.get("content") or "") additional_kwargs: dict = {} if _dict.get("function_call"): function_call = dict(_dict["function_call"]) diff --git a/libs/partners/groq/pyproject.toml b/libs/partners/groq/pyproject.toml index 43ba1f4f4d3..34994509fef 100644 --- a/libs/partners/groq/pyproject.toml +++ b/libs/partners/groq/pyproject.toml @@ -47,52 +47,8 @@ docstring-code-format = true docstring-code-line-length = 100 [tool.ruff.lint] -select = [ - "A", # flake8-builtins - "B", # flake8-bugbear - "ASYNC", # flake8-async - "C4", # flake8-comprehensions - "COM", # flake8-commas - "D", # pydocstyle - "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 -] +select = ["ALL"] 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__ - "D203", - "D213", "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful @@ -101,11 +57,22 @@ ignore = [ "SLF001", # Private member access "UP007", # pyupgrade: non-pep604-annotation-union "UP045", # pyupgrade: non-pep604-annotation-optional + "PLR0911", + "PLR0912", + "C901", + + # TODO + "ERA001", + "ANN401", + "BLE001", + "TC002", + "TC003", ] unfixable = ["B028"] # People should intentionally tune the stacklevel [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.coverage.run] omit = ["tests/*"] @@ -122,4 +89,17 @@ asyncio_mode = "auto" "tests/**/*.py" = [ "S101", # Tests need assertions "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "PT011", + "PT030", + "PT031", + "PLR2004", + "ANN401", + "ARG001", + "ARG002", + + # TODO + "D", +] +"scripts/*.py" = [ + "INP001", # Not a package ] diff --git a/libs/partners/groq/scripts/__init__.py b/libs/partners/groq/scripts/__init__.py new file mode 100644 index 00000000000..a8560dba121 --- /dev/null +++ b/libs/partners/groq/scripts/__init__.py @@ -0,0 +1 @@ +"""Scripts for Ollama partner integration.""" diff --git a/libs/partners/groq/scripts/check_imports.py b/libs/partners/groq/scripts/check_imports.py index ec3fc6e95f5..d47eaf1fef8 100644 --- a/libs/partners/groq/scripts/check_imports.py +++ b/libs/partners/groq/scripts/check_imports.py @@ -1,3 +1,5 @@ +"""Check that all imports in a list of files succeed.""" + import sys import traceback from importlib.machinery import SourceFileLoader diff --git a/libs/partners/groq/tests/integration_tests/test_chat_models.py b/libs/partners/groq/tests/integration_tests/test_chat_models.py index e35e1108505..613e5d2f28d 100644 --- a/libs/partners/groq/tests/integration_tests/test_chat_models.py +++ b/libs/partners/groq/tests/integration_tests/test_chat_models.py @@ -262,7 +262,7 @@ def test_reasoning_output_stream() -> None: full_response = token else: # Casting since adding results in a type error - full_response = cast(AIMessageChunk, full_response + token) + full_response = cast("AIMessageChunk", full_response + token) assert full_response is not None assert isinstance(full_response, AIMessageChunk) @@ -281,7 +281,8 @@ def test_reasoning_effort_none() -> None: response = chat.invoke([message]) assert isinstance(response, AIMessage) assert "reasoning_content" not in response.additional_kwargs - assert "" not in response.content and "" not in response.content + assert "" not in response.content + assert "" not in response.content @pytest.mark.parametrize("effort", ["low", "medium", "high"]) diff --git a/libs/partners/groq/tests/unit_tests/test_imports.py b/libs/partners/groq/tests/unit_tests/test_imports.py index eba04950b16..5c362ebcee7 100644 --- a/libs/partners/groq/tests/unit_tests/test_imports.py +++ b/libs/partners/groq/tests/unit_tests/test_imports.py @@ -4,4 +4,5 @@ EXPECTED_ALL = ["ChatGroq", "__version__"] def test_all_imports() -> None: + """Test that all expected imports are present in `__all__`.""" assert sorted(EXPECTED_ALL) == sorted(__all__) diff --git a/libs/partners/groq/tests/unit_tests/test_standard.py b/libs/partners/groq/tests/unit_tests/test_standard.py index 30e4b1679fb..5bcdfc869b8 100644 --- a/libs/partners/groq/tests/unit_tests/test_standard.py +++ b/libs/partners/groq/tests/unit_tests/test_standard.py @@ -9,6 +9,8 @@ from langchain_groq import ChatGroq class TestGroqStandard(ChatModelUnitTests): + """Run ChatGroq on LangChain standard tests.""" + @property def chat_model_class(self) -> type[BaseChatModel]: return ChatGroq diff --git a/libs/partners/huggingface/langchain_huggingface/utils/import_utils.py b/libs/partners/huggingface/langchain_huggingface/utils/import_utils.py index 4ce093eb43f..f086e327d0b 100644 --- a/libs/partners/huggingface/langchain_huggingface/utils/import_utils.py +++ b/libs/partners/huggingface/langchain_huggingface/utils/import_utils.py @@ -50,12 +50,12 @@ def compare_versions( ) -> bool: """Compare a library version to some requirement using a given operation. - Arguments: - library_or_version (`str` or `packaging.version.Version`): + Args: + library_or_version: A library name or a version to check. - operation (`str`): + operation: A string representation of an operator, such as `">"` or `"<="`. - requirement_version (`str`): + requirement_version: The version to compare the library version against """ diff --git a/libs/partners/huggingface/pyproject.toml b/libs/partners/huggingface/pyproject.toml index 101c9572b64..71a6f58a2e8 100644 --- a/libs/partners/huggingface/pyproject.toml +++ b/libs/partners/huggingface/pyproject.toml @@ -106,9 +106,6 @@ ignore = [ "D104", # pydocstyle: Missing docstring in public package "D105", # pydocstyle: Missing docstring in magic method "D107", # pydocstyle: Missing docstring in __init__ - "D203", - "D213", - "D407", # pydocstyle: Missing-dashed-underline-after-section "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful @@ -122,6 +119,7 @@ unfixable = ["B028"] # People should intentionally tune the stacklevel [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.coverage.run] omit = ["tests/*"] diff --git a/libs/partners/mistralai/langchain_mistralai/chat_models.py b/libs/partners/mistralai/langchain_mistralai/chat_models.py index 971b79eb1de..ff1735b5781 100644 --- a/libs/partners/mistralai/langchain_mistralai/chat_models.py +++ b/libs/partners/mistralai/langchain_mistralai/chat_models.py @@ -7,10 +7,9 @@ import os import re import ssl import uuid -from collections.abc import AsyncIterator, Iterator, Sequence -from contextlib import AbstractAsyncContextManager from operator import itemgetter from typing import ( + TYPE_CHECKING, Any, Callable, Literal, @@ -77,6 +76,10 @@ from pydantic import ( ) from typing_extensions import Self +if TYPE_CHECKING: + from collections.abc import AsyncIterator, Iterator, Sequence + from contextlib import AbstractAsyncContextManager + logger = logging.getLogger(__name__) # Mistral enforces a specific pattern for tool call IDs @@ -139,7 +142,7 @@ def _convert_mistral_chat_message_to_message( if role != "assistant": msg = f"Expected role to be 'assistant', got {role}" raise ValueError(msg) - content = cast(str, _message["content"]) + content = cast("str", _message["content"]) additional_kwargs: dict = {} tool_calls = [] @@ -149,7 +152,7 @@ def _convert_mistral_chat_message_to_message( for raw_tool_call in raw_tool_calls: try: parsed: dict = cast( - dict, parse_tool_call(raw_tool_call, return_id=True) + "dict", parse_tool_call(raw_tool_call, return_id=True) ) if not parsed["id"]: parsed["id"] = uuid.uuid4().hex[:] @@ -516,7 +519,7 @@ class ChatMistralAI(BaseChatModel): else: api_key_str = self.mistral_api_key - # todo: handle retries + # TODO: handle retries base_url_str = ( self.endpoint or os.environ.get("MISTRAL_BASE_URL") @@ -534,7 +537,7 @@ class ChatMistralAI(BaseChatModel): timeout=self.timeout, verify=global_ssl_context, ) - # todo: handle retries and max_concurrency + # TODO: handle retries and max_concurrency if not self.async_client: self.async_client = httpx.AsyncClient( base_url=base_url_str, @@ -639,7 +642,7 @@ class ChatMistralAI(BaseChatModel): gen_chunk = ChatGenerationChunk(message=new_chunk) if run_manager: run_manager.on_llm_new_token( - token=cast(str, new_chunk.content), chunk=gen_chunk + token=cast("str", new_chunk.content), chunk=gen_chunk ) yield gen_chunk @@ -665,7 +668,7 @@ class ChatMistralAI(BaseChatModel): gen_chunk = ChatGenerationChunk(message=new_chunk) if run_manager: await run_manager.on_llm_new_token( - token=cast(str, new_chunk.content), chunk=gen_chunk + token=cast("str", new_chunk.content), chunk=gen_chunk ) yield gen_chunk diff --git a/libs/partners/mistralai/langchain_mistralai/embeddings.py b/libs/partners/mistralai/langchain_mistralai/embeddings.py index 2632a4dbe3e..ad4f69c0e99 100644 --- a/libs/partners/mistralai/langchain_mistralai/embeddings.py +++ b/libs/partners/mistralai/langchain_mistralai/embeddings.py @@ -155,7 +155,7 @@ class MistralAIEmbeddings(BaseModel, Embeddings): def validate_environment(self) -> Self: """Validate configuration.""" api_key_str = self.mistral_api_key.get_secret_value() - # todo: handle retries + # TODO: handle retries if not self.client: self.client = httpx.Client( base_url=self.endpoint, @@ -166,7 +166,7 @@ class MistralAIEmbeddings(BaseModel, Embeddings): }, timeout=self.timeout, ) - # todo: handle retries and max_concurrency + # TODO: handle retries and max_concurrency if not self.async_client: self.async_client = httpx.AsyncClient( base_url=self.endpoint, @@ -255,8 +255,8 @@ class MistralAIEmbeddings(BaseModel, Embeddings): for response in batch_responses for embedding_obj in response.json()["data"] ] - except Exception as e: - logger.error(f"An error occurred with MistralAI: {e}") + except Exception: + logger.exception("An error occurred with MistralAI") raise async def aembed_documents(self, texts: list[str]) -> list[list[float]]: @@ -287,8 +287,8 @@ class MistralAIEmbeddings(BaseModel, Embeddings): for response in batch_responses for embedding_obj in response.json()["data"] ] - except Exception as e: - logger.error(f"An error occurred with MistralAI: {e}") + except Exception: + logger.exception("An error occurred with MistralAI") raise def embed_query(self, text: str) -> list[float]: diff --git a/libs/partners/mistralai/pyproject.toml b/libs/partners/mistralai/pyproject.toml index 03a0ffc541b..cdb89d21279 100644 --- a/libs/partners/mistralai/pyproject.toml +++ b/libs/partners/mistralai/pyproject.toml @@ -50,52 +50,8 @@ target-version = "py39" docstring-code-format = true [tool.ruff.lint] -select = [ - "A", # flake8-builtins - "B", # flake8-bugbear - "ASYNC", # flake8-async - "C4", # flake8-comprehensions - "COM", # flake8-commas - "D", # pydocstyle - "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 -] +select = ["ALL"] ignore = [ - "D100", # pydocstyle: Missing docstring in public module - "D101", # pydocstyle: Missing docstring in public class - "D102", # pydocstyle: Missing docstring in public method - "D103", # pydocstyle: Missing docstring in public function - "D104", # pydocstyle: Missing docstring in public package - "D105", # pydocstyle: Missing docstring in magic method - "D107", # pydocstyle: Missing docstring in __init__ - "D203", # Messes with the formatter - "D407", # pydocstyle: Missing-dashed-underline-after-section "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful @@ -104,11 +60,29 @@ ignore = [ "SLF001", # Private member access "UP007", # pyupgrade: non-pep604-annotation-union "UP045", # pyupgrade: non-pep604-annotation-optional + "TD", + "PLR0912", + "C901", + "FIX", + + # TODO + "TC002", + "ANN401", + "ARG001", + "ARG002", + "PT011", + "PLC0415", + "PLR2004", + "BLE001", + "D100", + "D102", + "D104", ] unfixable = ["B028"] # People should intentionally tune the stacklevel [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.coverage.run] omit = ["tests/*"] @@ -125,4 +99,9 @@ asyncio_mode = "auto" "tests/**/*.py" = [ "S101", # Tests need assertions "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "PLR2004", + "D", +] +"scripts/*.py" = [ + "INP001", # Not a package ] diff --git a/libs/partners/mistralai/tests/integration_tests/test_chat_models.py b/libs/partners/mistralai/tests/integration_tests/test_chat_models.py index df446b02b47..3e162d61063 100644 --- a/libs/partners/mistralai/tests/integration_tests/test_chat_models.py +++ b/libs/partners/mistralai/tests/integration_tests/test_chat_models.py @@ -320,6 +320,7 @@ def test_retry_parameters(caplog: pytest.LogCaptureFixture) -> None: # Measure start time t0 = time.time() + logger = logging.getLogger(__name__) try: # Try to get a response @@ -327,7 +328,7 @@ def test_retry_parameters(caplog: pytest.LogCaptureFixture) -> None: # If successful, validate the response elapsed_time = time.time() - t0 - logging.info(f"Request succeeded in {elapsed_time:.2f} seconds") + logger.info("Request succeeded in %.2f seconds", elapsed_time) # Check that we got a valid response assert response.content assert isinstance(response.content, str) @@ -335,9 +336,9 @@ def test_retry_parameters(caplog: pytest.LogCaptureFixture) -> None: except ReadTimeout: elapsed_time = time.time() - t0 - logging.info(f"Request timed out after {elapsed_time:.2f} seconds") + logger.info("Request timed out after %.2f seconds", elapsed_time) assert elapsed_time >= 3.0 pytest.skip("Test timed out as expected with short timeout") - except Exception as e: - logging.error(f"Unexpected exception: {e}") + except Exception: + logger.exception("Unexpected exception") raise diff --git a/libs/partners/mistralai/tests/unit_tests/test_chat_models.py b/libs/partners/mistralai/tests/unit_tests/test_chat_models.py index a633b7d2696..7851275ed94 100644 --- a/libs/partners/mistralai/tests/unit_tests/test_chat_models.py +++ b/libs/partners/mistralai/tests/unit_tests/test_chat_models.py @@ -43,11 +43,11 @@ def test_mistralai_initialization() -> None: ChatMistralAI(model="test", mistral_api_key="test"), # type: ignore[call-arg, call-arg] ChatMistralAI(model="test", api_key="test"), # type: ignore[call-arg, arg-type] ]: - assert cast(SecretStr, model.mistral_api_key).get_secret_value() == "test" + assert cast("SecretStr", model.mistral_api_key).get_secret_value() == "test" @pytest.mark.parametrize( - "model,expected_url", + ("model", "expected_url"), [ (ChatMistralAI(model="test"), "https://api.mistral.ai/v1"), # type: ignore[call-arg, arg-type] (ChatMistralAI(model="test", endpoint="baz"), "baz"), # type: ignore[call-arg, arg-type] diff --git a/libs/partners/mistralai/tests/unit_tests/test_embeddings.py b/libs/partners/mistralai/tests/unit_tests/test_embeddings.py index 41023f015e2..3b4a2472fae 100644 --- a/libs/partners/mistralai/tests/unit_tests/test_embeddings.py +++ b/libs/partners/mistralai/tests/unit_tests/test_embeddings.py @@ -14,4 +14,4 @@ def test_mistral_init() -> None: MistralAIEmbeddings(model="mistral-embed", api_key="test"), # type: ignore[arg-type] ]: assert model.model == "mistral-embed" - assert cast(SecretStr, model.mistral_api_key).get_secret_value() == "test" + assert cast("SecretStr", model.mistral_api_key).get_secret_value() == "test" diff --git a/libs/partners/nomic/langchain_nomic/__init__.py b/libs/partners/nomic/langchain_nomic/__init__.py index 01326dd75cd..192ad5db159 100644 --- a/libs/partners/nomic/langchain_nomic/__init__.py +++ b/libs/partners/nomic/langchain_nomic/__init__.py @@ -1,3 +1,5 @@ +"""Nomic partner integration for LangChain.""" + from langchain_nomic.embeddings import NomicEmbeddings __all__ = ["NomicEmbeddings"] diff --git a/libs/partners/nomic/langchain_nomic/embeddings.py b/libs/partners/nomic/langchain_nomic/embeddings.py index fb59d0bc3de..454c8847ffe 100644 --- a/libs/partners/nomic/langchain_nomic/embeddings.py +++ b/libs/partners/nomic/langchain_nomic/embeddings.py @@ -1,3 +1,5 @@ +"""Nomic partner integration for LangChain.""" + from __future__ import annotations import os @@ -29,7 +31,7 @@ class NomicEmbeddings(Embeddings): nomic_api_key: Optional[str] = ..., dimensionality: Optional[int] = ..., inference_mode: Literal["remote"] = ..., - ): ... + ) -> None: ... @overload def __init__( @@ -40,7 +42,7 @@ class NomicEmbeddings(Embeddings): dimensionality: Optional[int] = ..., inference_mode: Literal["local", "dynamic"], device: Optional[str] = ..., - ): ... + ) -> None: ... @overload def __init__( @@ -51,7 +53,7 @@ class NomicEmbeddings(Embeddings): dimensionality: Optional[int] = ..., inference_mode: str, device: Optional[str] = ..., - ): ... + ) -> None: ... def __init__( self, @@ -133,6 +135,11 @@ class NomicEmbeddings(Embeddings): )[0] def embed_image(self, uris: list[str]) -> list[list[float]]: + """Embed images. + + Args: + uris: list of image URIs to embed + """ return embed.image( images=uris, model=self.vision_model, diff --git a/libs/partners/nomic/pyproject.toml b/libs/partners/nomic/pyproject.toml index 299ccf120a7..6d1a924e730 100644 --- a/libs/partners/nomic/pyproject.toml +++ b/libs/partners/nomic/pyproject.toml @@ -49,52 +49,8 @@ target-version = "py39" docstring-code-format = true [tool.ruff.lint] -select = [ - "A", # flake8-builtins - "B", # flake8-bugbear - "ASYNC", # flake8-async - "C4", # flake8-comprehensions - "COM", # flake8-commas - "D", # pydocstyle - "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 -] +select = ["ALL"] ignore = [ - "D100", # pydocstyle: Missing docstring in public module - "D101", # pydocstyle: Missing docstring in public class - "D102", # pydocstyle: Missing docstring in public method - "D103", # pydocstyle: Missing docstring in public function - "D104", # pydocstyle: Missing docstring in public package - "D105", # pydocstyle: Missing docstring in magic method - "D107", # pydocstyle: Missing docstring in __init__ - "D203", # Messes with the formatter - "D407", # pydocstyle: Missing-dashed-underline-after-section "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful @@ -103,11 +59,15 @@ ignore = [ "SLF001", # Private member access "UP007", # pyupgrade: non-pep604-annotation-union "UP045", # pyupgrade: non-pep604-annotation-optional + + # TODO + "PLR0913", ] unfixable = ["B028"] # People should intentionally tune the stacklevel [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.mypy] disallow_untyped_defs = "True" @@ -138,4 +98,8 @@ asyncio_mode = "auto" "tests/**/*.py" = [ "S101", # Tests need assertions "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "PLR2004", +] +"scripts/*.py" = [ + "INP001", # Not a package ] diff --git a/libs/partners/nomic/scripts/check_imports.py b/libs/partners/nomic/scripts/check_imports.py index 58a460c1493..95b2f16ce34 100644 --- a/libs/partners/nomic/scripts/check_imports.py +++ b/libs/partners/nomic/scripts/check_imports.py @@ -1,3 +1,5 @@ +"""Script to check imports in Nomic partner integration.""" + import sys import traceback from importlib.machinery import SourceFileLoader @@ -8,7 +10,7 @@ if __name__ == "__main__": for file in files: try: SourceFileLoader("x", file).load_module() - except Exception: + except Exception: # noqa: BLE001 has_failure = True print(file) # noqa: T201 traceback.print_exc() diff --git a/libs/partners/nomic/tests/__init__.py b/libs/partners/nomic/tests/__init__.py index e69de29bb2d..cd17aeccbc8 100644 --- a/libs/partners/nomic/tests/__init__.py +++ b/libs/partners/nomic/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for Nomic partner integration.""" diff --git a/libs/partners/nomic/tests/integration_tests/__init__.py b/libs/partners/nomic/tests/integration_tests/__init__.py index e69de29bb2d..e307e4aa3cc 100644 --- a/libs/partners/nomic/tests/integration_tests/__init__.py +++ b/libs/partners/nomic/tests/integration_tests/__init__.py @@ -0,0 +1 @@ +"""Integration tests for Nomic partner integration.""" diff --git a/libs/partners/nomic/tests/integration_tests/test_compile.py b/libs/partners/nomic/tests/integration_tests/test_compile.py index f315e45f521..f748c3c89e0 100644 --- a/libs/partners/nomic/tests/integration_tests/test_compile.py +++ b/libs/partners/nomic/tests/integration_tests/test_compile.py @@ -1,3 +1,5 @@ +"""Test compilation of integration tests for Nomic partner integration.""" + import pytest diff --git a/libs/partners/nomic/tests/unit_tests/__init__.py b/libs/partners/nomic/tests/unit_tests/__init__.py index e69de29bb2d..6b67d39619f 100644 --- a/libs/partners/nomic/tests/unit_tests/__init__.py +++ b/libs/partners/nomic/tests/unit_tests/__init__.py @@ -0,0 +1 @@ +"""Unit tests for imports in Nomic partner integration.""" diff --git a/libs/partners/nomic/tests/unit_tests/test_imports.py b/libs/partners/nomic/tests/unit_tests/test_imports.py index 342069a8160..b292abb37fa 100644 --- a/libs/partners/nomic/tests/unit_tests/test_imports.py +++ b/libs/partners/nomic/tests/unit_tests/test_imports.py @@ -1,3 +1,5 @@ +"""Unit tests for imports in Nomic partner integration.""" + from langchain_nomic import __all__ EXPECTED_ALL = [ @@ -6,4 +8,5 @@ EXPECTED_ALL = [ def test_all_imports() -> None: + """Test that all expected imports are present in `__all__`.""" assert sorted(EXPECTED_ALL) == sorted(__all__) diff --git a/libs/partners/nomic/tests/unit_tests/test_standard.py b/libs/partners/nomic/tests/unit_tests/test_standard.py index a731ffc2e71..30d0e6d0d3a 100644 --- a/libs/partners/nomic/tests/unit_tests/test_standard.py +++ b/libs/partners/nomic/tests/unit_tests/test_standard.py @@ -1,3 +1,5 @@ +"""Unit tests for standard tests in Nomic partner integration.""" + import pytest from pytest_benchmark.fixture import BenchmarkFixture # type: ignore[import] diff --git a/libs/partners/ollama/langchain_ollama/chat_models.py b/libs/partners/ollama/langchain_ollama/chat_models.py index 6c0504bd761..6d7dc66373a 100644 --- a/libs/partners/ollama/langchain_ollama/chat_models.py +++ b/libs/partners/ollama/langchain_ollama/chat_models.py @@ -725,7 +725,7 @@ class ChatOllama(BaseChatModel): tool_call_id = message.tool_call_id else: msg = "Received unsupported message type for Ollama." - raise ValueError(msg) + raise TypeError(msg) content = "" images = [] @@ -814,9 +814,8 @@ class ChatOllama(BaseChatModel): if chat_params["stream"]: if self._client: yield from self._client.chat(**chat_params) - else: - if self._client: - yield self._client.chat(**chat_params) + elif self._client: + yield self._client.chat(**chat_params) def _chat_stream_with_aggregation( self, @@ -899,8 +898,10 @@ class ChatOllama(BaseChatModel): chat_generation = ChatGeneration( message=AIMessage( content=final_chunk.text, - usage_metadata=cast(AIMessageChunk, final_chunk.message).usage_metadata, - tool_calls=cast(AIMessageChunk, final_chunk.message).tool_calls, + usage_metadata=cast( + "AIMessageChunk", final_chunk.message + ).usage_metadata, + tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls, additional_kwargs=final_chunk.message.additional_kwargs, ), generation_info=generation_info, @@ -1073,8 +1074,10 @@ class ChatOllama(BaseChatModel): chat_generation = ChatGeneration( message=AIMessage( content=final_chunk.text, - usage_metadata=cast(AIMessageChunk, final_chunk.message).usage_metadata, - tool_calls=cast(AIMessageChunk, final_chunk.message).tool_calls, + usage_metadata=cast( + "AIMessageChunk", final_chunk.message + ).usage_metadata, + tool_calls=cast("AIMessageChunk", final_chunk.message).tool_calls, additional_kwargs=final_chunk.message.additional_kwargs, ), generation_info=generation_info, @@ -1090,7 +1093,7 @@ class ChatOllama(BaseChatModel): self, tools: Sequence[Union[dict[str, Any], type, Callable, BaseTool]], *, - tool_choice: Optional[Union[dict, str, Literal["auto", "any"], bool]] = None, # noqa: PYI051 + tool_choice: Optional[Union[dict, str, Literal["auto", "any"], bool]] = None, # noqa: PYI051, ARG002 **kwargs: Any, ) -> Runnable[LanguageModelInput, BaseMessage]: """Bind tool-like objects to this chat model. @@ -1117,7 +1120,7 @@ class ChatOllama(BaseChatModel): include_raw: bool = False, **kwargs: Any, ) -> Runnable[LanguageModelInput, Union[dict, BaseModel]]: - """Model wrapper that returns outputs formatted to match the given schema. + r"""Model wrapper that returns outputs formatted to match the given schema. Args: schema: The output schema. Can be passed in as: @@ -1351,7 +1354,7 @@ class ChatOllama(BaseChatModel): # 'parsing_error': None # } - """ # noqa: E501, D301 + """ # noqa: E501 _ = kwargs.pop("strict", None) if kwargs: msg = f"Received unsupported arguments {kwargs}" @@ -1404,7 +1407,7 @@ class ChatOllama(BaseChatModel): ) raise ValueError(msg) if is_pydantic_schema: - schema = cast(TypeBaseModel, schema) + schema = cast("TypeBaseModel", schema) if issubclass(schema, BaseModelV1): response_format = schema.schema() else: @@ -1426,7 +1429,7 @@ class ChatOllama(BaseChatModel): ) else: # is JSON schema - response_format = cast(dict, schema) + response_format = cast("dict", schema) llm = self.bind( format=response_format, ls_structured_output_format={ diff --git a/libs/partners/ollama/pyproject.toml b/libs/partners/ollama/pyproject.toml index 393fedee923..55f9f662d51 100644 --- a/libs/partners/ollama/pyproject.toml +++ b/libs/partners/ollama/pyproject.toml @@ -50,52 +50,8 @@ docstring-code-format = true docstring-code-line-length = 100 [tool.ruff.lint] -select = [ - "A", # flake8-builtins - "B", # flake8-bugbear - "ASYNC", # flake8-async - "C4", # flake8-comprehensions - "COM", # flake8-commas - "D", # pydocstyle - "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 -] +select = ["ALL"] ignore = [ - "D100", # pydocstyle: Missing docstring in public module - "D101", # pydocstyle: Missing docstring in public class - "D102", # pydocstyle: Missing docstring in public method - "D103", # pydocstyle: Missing docstring in public function - "D104", # pydocstyle: Missing docstring in public package - "D105", # pydocstyle: Missing docstring in magic method - "D107", # pydocstyle: Missing docstring in __init__ - "D203", # Messes with the formatter - "D407", # pydocstyle: Missing-dashed-underline-after-section "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful @@ -104,11 +60,21 @@ ignore = [ "SLF001", # Private member access "UP007", # pyupgrade: non-pep604-annotation-union "UP045", # pyupgrade: non-pep604-annotation-optional + "PLR0912", + "C901", + "PLR0915", + + # TODO: + "ANN401", + "TC002", + "TC003", + "TRY301", ] unfixable = ["B028"] # People should intentionally tune the stacklevel [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.ruff.lint.per-file-ignores] "tests/**" = ["D"] # ignore docstring checks for tests @@ -127,4 +93,15 @@ asyncio_mode = "auto" "tests/**/*.py" = [ "S101", # Tests need assertions "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "PLR2004", + + # TODO + "ANN401", + "ARG001", + "PT011", + "FIX", + "TD", +] +"scripts/*.py" = [ + "INP001", # Not a package ] diff --git a/libs/partners/ollama/scripts/check_imports.py b/libs/partners/ollama/scripts/check_imports.py index 64b1b383092..900b5cbc07e 100644 --- a/libs/partners/ollama/scripts/check_imports.py +++ b/libs/partners/ollama/scripts/check_imports.py @@ -10,7 +10,7 @@ if __name__ == "__main__": for file in files: try: SourceFileLoader("x", file).load_module() - except Exception: + except Exception: # noqa: BLE001 has_failure = True print(file) # noqa: T201 traceback.print_exc() diff --git a/libs/partners/ollama/tests/integration_tests/chat_models/__init__.py b/libs/partners/ollama/tests/integration_tests/chat_models/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/partners/ollama/tests/integration_tests/chat_models/test_chat_models_reasoning.py b/libs/partners/ollama/tests/integration_tests/chat_models/test_chat_models_reasoning.py index 82198050b40..86ba74c0069 100644 --- a/libs/partners/ollama/tests/integration_tests/chat_models/test_chat_models_reasoning.py +++ b/libs/partners/ollama/tests/integration_tests/chat_models/test_chat_models_reasoning.py @@ -27,7 +27,8 @@ def test_stream_no_reasoning(model: str) -> None: result += chunk assert isinstance(result, AIMessageChunk) assert result.content - assert "" not in result.content and "" not in result.content + assert "" not in result.content + assert "" not in result.content assert "reasoning_content" not in result.additional_kwargs @@ -50,7 +51,8 @@ async def test_astream_no_reasoning(model: str) -> None: result += chunk assert isinstance(result, AIMessageChunk) assert result.content - assert "" not in result.content and "" not in result.content + assert "" not in result.content + assert "" not in result.content assert "reasoning_content" not in result.additional_kwargs @@ -73,7 +75,8 @@ def test_stream_reasoning_none(model: str) -> None: result += chunk assert isinstance(result, AIMessageChunk) assert result.content - assert "" in result.content and "" in result.content + assert "" in result.content + assert "" in result.content assert "reasoning_content" not in result.additional_kwargs assert "" not in result.additional_kwargs.get("reasoning_content", "") assert "" not in result.additional_kwargs.get("reasoning_content", "") @@ -98,7 +101,8 @@ async def test_astream_reasoning_none(model: str) -> None: result += chunk assert isinstance(result, AIMessageChunk) assert result.content - assert "" in result.content and "" in result.content + assert "" in result.content + assert "" in result.content assert "reasoning_content" not in result.additional_kwargs assert "" not in result.additional_kwargs.get("reasoning_content", "") assert "" not in result.additional_kwargs.get("reasoning_content", "") @@ -125,7 +129,8 @@ def test_reasoning_stream(model: str) -> None: assert result.content assert "reasoning_content" in result.additional_kwargs assert len(result.additional_kwargs["reasoning_content"]) > 0 - assert "" not in result.content and "" not in result.content + assert "" not in result.content + assert "" not in result.content assert "" not in result.additional_kwargs["reasoning_content"] assert "" not in result.additional_kwargs["reasoning_content"] @@ -151,7 +156,8 @@ async def test_reasoning_astream(model: str) -> None: assert result.content assert "reasoning_content" in result.additional_kwargs assert len(result.additional_kwargs["reasoning_content"]) > 0 - assert "" not in result.content and "" not in result.content + assert "" not in result.content + assert "" not in result.content assert "" not in result.additional_kwargs["reasoning_content"] assert "" not in result.additional_kwargs["reasoning_content"] @@ -164,7 +170,8 @@ def test_invoke_no_reasoning(model: str) -> None: result = llm.invoke([message]) assert result.content assert "reasoning_content" not in result.additional_kwargs - assert "" not in result.content and "" not in result.content + assert "" not in result.content + assert "" not in result.content @pytest.mark.parametrize(("model"), [("deepseek-r1:1.5b")]) @@ -175,7 +182,8 @@ async def test_ainvoke_no_reasoning(model: str) -> None: result = await llm.ainvoke([message]) assert result.content assert "reasoning_content" not in result.additional_kwargs - assert "" not in result.content and "" not in result.content + assert "" not in result.content + assert "" not in result.content @pytest.mark.parametrize(("model"), [("deepseek-r1:1.5b")]) @@ -186,7 +194,8 @@ def test_invoke_reasoning_none(model: str) -> None: result = llm.invoke([message]) assert result.content assert "reasoning_content" not in result.additional_kwargs - assert "" in result.content and "" in result.content + assert "" in result.content + assert "" in result.content assert "" not in result.additional_kwargs.get("reasoning_content", "") assert "" not in result.additional_kwargs.get("reasoning_content", "") @@ -199,7 +208,8 @@ async def test_ainvoke_reasoning_none(model: str) -> None: result = await llm.ainvoke([message]) assert result.content assert "reasoning_content" not in result.additional_kwargs - assert "" in result.content and "" in result.content + assert "" in result.content + assert "" in result.content assert "" not in result.additional_kwargs.get("reasoning_content", "") assert "" not in result.additional_kwargs.get("reasoning_content", "") @@ -213,7 +223,8 @@ def test_reasoning_invoke(model: str) -> None: assert result.content assert "reasoning_content" in result.additional_kwargs assert len(result.additional_kwargs["reasoning_content"]) > 0 - assert "" not in result.content and "" not in result.content + assert "" not in result.content + assert "" not in result.content assert "" not in result.additional_kwargs["reasoning_content"] assert "" not in result.additional_kwargs["reasoning_content"] @@ -227,7 +238,8 @@ async def test_reasoning_ainvoke(model: str) -> None: assert result.content assert "reasoning_content" in result.additional_kwargs assert len(result.additional_kwargs["reasoning_content"]) > 0 - assert "" not in result.content and "" not in result.content + assert "" not in result.content + assert "" not in result.content assert "" not in result.additional_kwargs["reasoning_content"] assert "" not in result.additional_kwargs["reasoning_content"] diff --git a/libs/partners/ollama/tests/integration_tests/test_llms.py b/libs/partners/ollama/tests/integration_tests/test_llms.py index 34136f0862d..9989309d9ee 100644 --- a/libs/partners/ollama/tests/integration_tests/test_llms.py +++ b/libs/partners/ollama/tests/integration_tests/test_llms.py @@ -61,7 +61,8 @@ def test__stream_with_reasoning(model: str) -> None: assert "reasoning_content" in result_chunk.generation_info # type: ignore[operator] assert len(result_chunk.generation_info["reasoning_content"]) > 0 # type: ignore[index] # And neither the visible nor the hidden portion contains tags - assert "" not in result_chunk.text and "" not in result_chunk.text + assert "" not in result_chunk.text + assert "" not in result_chunk.text assert "" not in result_chunk.generation_info["reasoning_content"] # type: ignore[index] assert "" not in result_chunk.generation_info["reasoning_content"] # type: ignore[index] diff --git a/libs/partners/ollama/tests/unit_tests/test_chat_models.py b/libs/partners/ollama/tests/unit_tests/test_chat_models.py index 5bcb8262908..640d7e66c8d 100644 --- a/libs/partners/ollama/tests/unit_tests/test_chat_models.py +++ b/libs/partners/ollama/tests/unit_tests/test_chat_models.py @@ -141,7 +141,7 @@ dummy_raw_tool_call = { @pytest.mark.parametrize( - "input_string, expected_output", + ("input_string", "expected_output"), [ # Case 1: Standard double-quoted JSON ('{"key": "value", "number": 123}', {"key": "value", "number": 123}), diff --git a/libs/partners/openai/langchain_openai/__init__.py b/libs/partners/openai/langchain_openai/__init__.py index 40a94c25ce1..da0c6c2b25d 100644 --- a/libs/partners/openai/langchain_openai/__init__.py +++ b/libs/partners/openai/langchain_openai/__init__.py @@ -1,14 +1,16 @@ +"""Module for OpenAI integrations.""" + from langchain_openai.chat_models import AzureChatOpenAI, ChatOpenAI from langchain_openai.embeddings import AzureOpenAIEmbeddings, OpenAIEmbeddings from langchain_openai.llms import AzureOpenAI, OpenAI from langchain_openai.tools import custom_tool __all__ = [ - "OpenAI", - "ChatOpenAI", - "OpenAIEmbeddings", - "AzureOpenAI", "AzureChatOpenAI", + "AzureOpenAI", "AzureOpenAIEmbeddings", + "ChatOpenAI", + "OpenAI", + "OpenAIEmbeddings", "custom_tool", ] diff --git a/libs/partners/openai/langchain_openai/chat_models/__init__.py b/libs/partners/openai/langchain_openai/chat_models/__init__.py index 574128d2704..e43102ffabc 100644 --- a/libs/partners/openai/langchain_openai/chat_models/__init__.py +++ b/libs/partners/openai/langchain_openai/chat_models/__init__.py @@ -1,4 +1,6 @@ +"""Module for OpenAI chat models.""" + from langchain_openai.chat_models.azure import AzureChatOpenAI from langchain_openai.chat_models.base import ChatOpenAI -__all__ = ["ChatOpenAI", "AzureChatOpenAI"] +__all__ = ["AzureChatOpenAI", "ChatOpenAI"] diff --git a/libs/partners/openai/langchain_openai/chat_models/_client_utils.py b/libs/partners/openai/langchain_openai/chat_models/_client_utils.py index c41f500a454..87aa2ae2c80 100644 --- a/libs/partners/openai/langchain_openai/chat_models/_client_utils.py +++ b/libs/partners/openai/langchain_openai/chat_models/_client_utils.py @@ -6,6 +6,8 @@ for each instance of ChatOpenAI. Logic is largely replicated from openai._base_client. """ +from __future__ import annotations + import asyncio import os from functools import lru_cache @@ -15,7 +17,7 @@ import openai class _SyncHttpxClientWrapper(openai.DefaultHttpxClient): - """Borrowed from openai._base_client""" + """Borrowed from openai._base_client.""" def __del__(self) -> None: if self.is_closed: @@ -28,7 +30,7 @@ class _SyncHttpxClientWrapper(openai.DefaultHttpxClient): class _AsyncHttpxClientWrapper(openai.DefaultAsyncHttpxClient): - """Borrowed from openai._base_client""" + """Borrowed from openai._base_client.""" def __del__(self) -> None: if self.is_closed: diff --git a/libs/partners/openai/langchain_openai/chat_models/_compat.py b/libs/partners/openai/langchain_openai/chat_models/_compat.py index 25ff3eb607c..a5b7dcb569a 100644 --- a/libs/partners/openai/langchain_openai/chat_models/_compat.py +++ b/libs/partners/openai/langchain_openai/chat_models/_compat.py @@ -1,5 +1,4 @@ -""" -This module converts between AIMessage output formats for the Responses API. +"""This module converts between AIMessage output formats for the Responses API. ChatOpenAI v0.3 stores reasoning and tool outputs in AIMessage.additional_kwargs: @@ -59,7 +58,9 @@ content blocks, rather than on the AIMessage.id, which now stores the response I For backwards compatibility, this module provides functions to convert between the old and new formats. The functions are used internally by ChatOpenAI. -""" # noqa: E501 +""" + +from __future__ import annotations import json from typing import Union diff --git a/libs/partners/openai/langchain_openai/chat_models/azure.py b/libs/partners/openai/langchain_openai/chat_models/azure.py index ba24e3da8b2..0b2f36a8bc9 100644 --- a/libs/partners/openai/langchain_openai/chat_models/azure.py +++ b/libs/partners/openai/langchain_openai/chat_models/azure.py @@ -5,12 +5,11 @@ from __future__ import annotations import logging import os from collections.abc import AsyncIterator, Awaitable, Iterator -from typing import Any, Callable, Optional, TypedDict, TypeVar, Union +from typing import Any, Callable, Optional, TypeVar, Union import openai from langchain_core.language_models import LanguageModelInput from langchain_core.language_models.chat_models import LangSmithParams -from langchain_core.messages import BaseMessage from langchain_core.outputs import ChatGenerationChunk, ChatResult from langchain_core.runnables import Runnable from langchain_core.utils import from_env, secret_from_env @@ -28,18 +27,12 @@ _DictOrPydanticClass = Union[dict[str, Any], type[_BM]] _DictOrPydantic = Union[dict, _BM] -class _AllReturnType(TypedDict): - raw: BaseMessage - parsed: Optional[_DictOrPydantic] - parsing_error: Optional[BaseException] - - def _is_pydantic_class(obj: Any) -> bool: return isinstance(obj, type) and is_basemodel_subclass(obj) class AzureChatOpenAI(BaseChatOpenAI): - """Azure OpenAI chat model integration. + r"""Azure OpenAI chat model integration. Setup: Head to the Azure `OpenAI quickstart guide `__ @@ -588,6 +581,7 @@ class AzureChatOpenAI(BaseChatOpenAI): @property def lc_secrets(self) -> dict[str, str]: + """Get the mapping of secret environment variables.""" return { "openai_api_key": "AZURE_OPENAI_API_KEY", "azure_ad_token": "AZURE_OPENAI_AD_TOKEN", @@ -595,15 +589,18 @@ class AzureChatOpenAI(BaseChatOpenAI): @classmethod def is_lc_serializable(cls) -> bool: + """Check if the class is serializable in langchain.""" return True @model_validator(mode="after") def validate_environment(self) -> Self: """Validate that api key and python package exists in environment.""" if self.n is not None and self.n < 1: - raise ValueError("n must be at least 1.") - elif self.n is not None and self.n > 1 and self.streaming: - raise ValueError("n must be 1 when streaming.") + msg = "n must be at least 1." + raise ValueError(msg) + if self.n is not None and self.n > 1 and self.streaming: + msg = "n must be 1 when streaming." + raise ValueError(msg) if self.disabled_params is None: # As of 09-17-2024 'parallel_tool_calls' param is only supported for gpt-4o. @@ -623,13 +620,14 @@ class AzureChatOpenAI(BaseChatOpenAI): openai_api_base = self.openai_api_base if openai_api_base and self.validate_base_url: if "/openai" not in openai_api_base: - raise ValueError( + msg = ( "As of openai>=1.0.0, Azure endpoints should be specified via " "the `azure_endpoint` param not `openai_api_base` " "(or alias `base_url`)." ) + raise ValueError(msg) if self.deployment_name: - raise ValueError( + msg = ( "As of openai>=1.0.0, if `azure_deployment` (or alias " "`deployment_name`) is specified then " "`base_url` (or alias `openai_api_base`) should not be. " @@ -641,6 +639,7 @@ class AzureChatOpenAI(BaseChatOpenAI): "Or you can equivalently specify:\n\n" 'base_url="https://xxx.openai.azure.com/openai/deployments/my-deployment"' ) + raise ValueError(msg) client_params: dict = { "api_version": self.openai_api_version, "azure_endpoint": self.azure_endpoint, @@ -687,7 +686,7 @@ class AzureChatOpenAI(BaseChatOpenAI): def _identifying_params(self) -> dict[str, Any]: """Get the identifying parameters.""" return { - **{"azure_deployment": self.deployment_name}, + "azure_deployment": self.deployment_name, **super()._identifying_params, } @@ -697,6 +696,7 @@ class AzureChatOpenAI(BaseChatOpenAI): @property def lc_attributes(self) -> dict[str, Any]: + """Get the attributes relevant to tracing.""" return { "openai_api_type": self.openai_api_type, "openai_api_version": self.openai_api_version, @@ -739,10 +739,11 @@ class AzureChatOpenAI(BaseChatOpenAI): response = response.model_dump() for res in response["choices"]: if res.get("finish_reason", None) == "content_filter": - raise ValueError( + msg = ( "Azure has not provided the response due to a content filter " "being triggered" ) + raise ValueError(msg) if "model" in response: model = response["model"] @@ -790,8 +791,7 @@ class AzureChatOpenAI(BaseChatOpenAI): """Route to Chat Completions or Responses API.""" if self._use_responses_api({**kwargs, **self.model_kwargs}): return super()._stream_responses(*args, **kwargs) - else: - return super()._stream(*args, **kwargs) + return super()._stream(*args, **kwargs) async def _astream( self, *args: Any, **kwargs: Any @@ -813,7 +813,7 @@ class AzureChatOpenAI(BaseChatOpenAI): strict: Optional[bool] = None, **kwargs: Any, ) -> Runnable[LanguageModelInput, _DictOrPydantic]: - """Model wrapper that returns outputs formatted to match the given schema. + r"""Model wrapper that returns outputs formatted to match the given schema. Args: schema: The output schema. Can be passed in as: diff --git a/libs/partners/openai/langchain_openai/chat_models/base.py b/libs/partners/openai/langchain_openai/chat_models/base.py index 1ec35a7c179..e2d6867e69d 100644 --- a/libs/partners/openai/langchain_openai/chat_models/base.py +++ b/libs/partners/openai/langchain_openai/chat_models/base.py @@ -145,7 +145,7 @@ def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: id_ = _dict.get("id") if role == "user": return HumanMessage(content=_dict.get("content", ""), id=id_, name=name) - elif role == "assistant": + if role == "assistant": # Fix for azure # Also OpenAI returns None for tool invocations content = _dict.get("content", "") or "" @@ -173,22 +173,19 @@ def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: tool_calls=tool_calls, invalid_tool_calls=invalid_tool_calls, ) - elif role in ("system", "developer"): - if role == "developer": - additional_kwargs = {"__openai_role__": role} - else: - additional_kwargs = {} + if role in ("system", "developer"): + additional_kwargs = {"__openai_role__": role} if role == "developer" else {} return SystemMessage( content=_dict.get("content", ""), name=name, id=id_, additional_kwargs=additional_kwargs, ) - elif role == "function": + if role == "function": return FunctionMessage( content=_dict.get("content", ""), name=cast(str, _dict.get("name")), id=id_ ) - elif role == "tool": + if role == "tool": additional_kwargs = {} if "name" in _dict: additional_kwargs["name"] = _dict["name"] @@ -199,8 +196,7 @@ def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: name=name, id=id_, ) - else: - return ChatMessage(content=_dict.get("content", ""), role=role, id=id_) # type: ignore[arg-type] + return ChatMessage(content=_dict.get("content", ""), role=role, id=id_) # type: ignore[arg-type] def _format_message_content(content: Any) -> Any: @@ -215,7 +211,7 @@ def _format_message_content(content: Any) -> Any: and block["type"] in ("tool_use", "thinking", "reasoning_content") ): continue - elif isinstance(block, dict) and is_data_content_block(block): + if isinstance(block, dict) and is_data_content_block(block): formatted_content.append(convert_to_openai_data_block(block)) # Anthropic image blocks elif ( @@ -315,13 +311,15 @@ def _convert_message_to_dict(message: BaseMessage) -> dict: supported_props = {"content", "role", "tool_call_id"} message_dict = {k: v for k, v in message_dict.items() if k in supported_props} else: - raise TypeError(f"Got unknown type {message}") + msg = f"Got unknown type {message}" + raise TypeError(msg) return message_dict def _convert_delta_to_message_chunk( _dict: Mapping[str, Any], default_class: type[BaseMessageChunk] ) -> BaseMessageChunk: + """Convert to a LangChain message chunk.""" id_ = _dict.get("id") role = cast(str, _dict.get("role")) content = cast(str, _dict.get("content") or "") @@ -349,14 +347,14 @@ def _convert_delta_to_message_chunk( if role == "user" or default_class == HumanMessageChunk: return HumanMessageChunk(content=content, id=id_) - elif role == "assistant" or default_class == AIMessageChunk: + if role == "assistant" or default_class == AIMessageChunk: return AIMessageChunk( content=content, additional_kwargs=additional_kwargs, id=id_, tool_call_chunks=tool_call_chunks, # type: ignore[arg-type] ) - elif role in ("system", "developer") or default_class == SystemMessageChunk: + if role in ("system", "developer") or default_class == SystemMessageChunk: if role == "developer": additional_kwargs = {"__openai_role__": "developer"} else: @@ -364,16 +362,15 @@ def _convert_delta_to_message_chunk( return SystemMessageChunk( content=content, id=id_, additional_kwargs=additional_kwargs ) - elif role == "function" or default_class == FunctionMessageChunk: + if role == "function" or default_class == FunctionMessageChunk: return FunctionMessageChunk(content=content, name=_dict["name"], id=id_) - elif role == "tool" or default_class == ToolMessageChunk: + if role == "tool" or default_class == ToolMessageChunk: return ToolMessageChunk( content=content, tool_call_id=_dict["tool_call_id"], id=id_ ) - elif role or default_class == ChatMessageChunk: + if role or default_class == ChatMessageChunk: return ChatMessageChunk(content=content, role=role, id=id_) - else: - return default_class(content=content, id=id_) # type: ignore + return default_class(content=content, id=id_) # type: ignore[call-arg] def _update_token_usage( @@ -383,24 +380,25 @@ def _update_token_usage( # `reasoning_tokens` is nested inside `completion_tokens_details` if isinstance(new_usage, int): if not isinstance(overall_token_usage, int): - raise ValueError( + msg = ( f"Got different types for token usage: " f"{type(new_usage)} and {type(overall_token_usage)}" ) + raise ValueError(msg) return new_usage + overall_token_usage - elif isinstance(new_usage, dict): + if isinstance(new_usage, dict): if not isinstance(overall_token_usage, dict): - raise ValueError( + msg = ( f"Got different types for token usage: " f"{type(new_usage)} and {type(overall_token_usage)}" ) + raise ValueError(msg) return { k: _update_token_usage(overall_token_usage.get(k, 0), v) for k, v in new_usage.items() } - else: - warnings.warn(f"Unexpected type for token usage: {type(new_usage)}") - return new_usage + warnings.warn(f"Unexpected type for token usage: {type(new_usage)}") + return new_usage def _handle_openai_bad_request(e: openai.BadRequestError) -> None: @@ -415,18 +413,17 @@ def _handle_openai_bad_request(e: openai.BadRequestError) -> None: ) warnings.warn(message) raise e - elif "Invalid schema for response_format" in e.message: + if "Invalid schema for response_format" in e.message: message = ( "Invalid schema for OpenAI's structured output feature, which is the " "default method for `with_structured_output` as of langchain-openai==0.3. " 'Specify `method="function_calling"` instead or update your schema. ' "See supported schemas: " - "https://platform.openai.com/docs/guides/structured-outputs#supported-schemas" # noqa: E501 + "https://platform.openai.com/docs/guides/structured-outputs#supported-schemas" ) warnings.warn(message) raise e - else: - raise + raise class _FunctionCall(TypedDict): @@ -438,13 +435,9 @@ _DictOrPydanticClass = Union[dict[str, Any], type[_BM], type] _DictOrPydantic = Union[dict, _BM] -class _AllReturnType(TypedDict): - raw: BaseMessage - parsed: Optional[_DictOrPydantic] - parsing_error: Optional[BaseException] - - class BaseChatOpenAI(BaseChatModel): + """Base wrapper around OpenAI large language models for chat.""" + client: Any = Field(default=None, exclude=True) #: :meta private: async_client: Any = Field(default=None, exclude=True) #: :meta private: root_client: Any = Field(default=None, exclude=True) #: :meta private: @@ -708,8 +701,7 @@ class BaseChatOpenAI(BaseChatModel): def build_extra(cls, values: dict[str, Any]) -> Any: """Build extra kwargs from additional params that were passed in.""" 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="before") @classmethod @@ -741,9 +733,11 @@ class BaseChatOpenAI(BaseChatModel): def validate_environment(self) -> Self: """Validate that api key and python package exists in environment.""" if self.n is not None and self.n < 1: - raise ValueError("n must be at least 1.") - elif self.n is not None and self.n > 1 and self.streaming: - raise ValueError("n must be 1 when streaming.") + msg = "n must be at least 1." + raise ValueError(msg) + if self.n is not None and self.n > 1 and self.streaming: + msg = "n must be 1 when streaming." + raise ValueError(msg) # Check OPENAI_ORGANIZATION for backwards compatibility. self.openai_organization = ( @@ -769,20 +763,22 @@ class BaseChatOpenAI(BaseChatModel): openai_proxy = self.openai_proxy http_client = self.http_client http_async_client = self.http_async_client - raise ValueError( + msg = ( "Cannot specify 'openai_proxy' if one of " "'http_client'/'http_async_client' is already specified. Received:\n" f"{openai_proxy=}\n{http_client=}\n{http_async_client=}" ) + raise ValueError(msg) if not self.client: if self.openai_proxy and not self.http_client: try: import httpx except ImportError as e: - raise ImportError( + msg = ( "Could not import httpx python package. " "Please install it with `pip install httpx`." - ) from e + ) + raise ImportError(msg) from e self.http_client = httpx.Client( proxy=self.openai_proxy, verify=global_ssl_context ) @@ -797,10 +793,11 @@ class BaseChatOpenAI(BaseChatModel): try: import httpx except ImportError as e: - raise ImportError( + msg = ( "Could not import httpx python package. " "Please install it with `pip install httpx`." - ) from e + ) + raise ImportError(msg) from e self.http_async_client = httpx.AsyncClient( proxy=self.openai_proxy, verify=global_ssl_context ) @@ -842,15 +839,13 @@ class BaseChatOpenAI(BaseChatModel): "store": self.store, } - params = { + return { "model": self.model_name, "stream": self.streaming, **{k: v for k, v in exclude_if_none.items() if v is not None}, **self.model_kwargs, } - return params - def _combine_llm_outputs(self, llm_outputs: list[Optional[dict]]) -> dict: overall_token_usage: dict = {} system_fingerprint = None @@ -896,11 +891,10 @@ class BaseChatOpenAI(BaseChatModel): ) if len(choices) == 0: # logprobs is implicitly None - generation_chunk = ChatGenerationChunk( + return ChatGenerationChunk( message=default_chunk_class(content="", usage_metadata=usage_metadata), generation_info=base_generation_info, ) - return generation_chunk choice = choices[0] if choice["delta"] is None: @@ -927,10 +921,9 @@ class BaseChatOpenAI(BaseChatModel): if usage_metadata and isinstance(message_chunk, AIMessageChunk): message_chunk.usage_metadata = usage_metadata - generation_chunk = ChatGenerationChunk( + return ChatGenerationChunk( message=message_chunk, generation_info=generation_info or None ) - return generation_chunk def _stream_responses( self, @@ -1193,18 +1186,15 @@ class BaseChatOpenAI(BaseChatModel): def _use_responses_api(self, payload: dict) -> bool: if isinstance(self.use_responses_api, bool): return self.use_responses_api - elif self.output_version == "responses/v1": + if ( + self.output_version == "responses/v1" + or self.include is not None + or self.reasoning is not None + or self.truncation is not None + or self.use_previous_response_id + ): return True - elif self.include is not None: - return True - elif self.reasoning is not None: - return True - elif self.truncation is not None: - return True - elif self.use_previous_response_id: - return True - else: - return _use_responses_api(payload) + return _use_responses_api(payload) def _get_request_payload( self, @@ -1253,12 +1243,12 @@ class BaseChatOpenAI(BaseChatModel): try: choices = response_dict["choices"] except KeyError as e: - raise KeyError( - f"Response missing `choices` key: {response_dict.keys()}" - ) from e + msg = f"Response missing `choices` key: {response_dict.keys()}" + raise KeyError(msg) from e if choices is None: - raise TypeError("Received response with null value for `choices`.") + msg = "Received response with null value for `choices`." + raise TypeError(msg) token_usage = response_dict.get("usage") @@ -1551,20 +1541,17 @@ class BaseChatOpenAI(BaseChatModel): tokens_per_message = 4 # if there's a name, the role is omitted tokens_per_name = -1 - elif ( - model.startswith("gpt-3.5-turbo") - or model.startswith("gpt-4") - or model.startswith("gpt-5") - ): + elif model.startswith(("gpt-3.5-turbo", "gpt-4", "gpt-5")): tokens_per_message = 3 tokens_per_name = 1 else: - raise NotImplementedError( + msg = ( f"get_num_tokens_from_messages() is not presently implemented " f"for model {model}. See " - "https://platform.openai.com/docs/guides/text-generation/managing-tokens" # noqa: E501 + "https://platform.openai.com/docs/guides/text-generation/managing-tokens" " for information on how messages are converted to tokens." ) + raise NotImplementedError(msg) num_tokens = 0 messages_dict = [_convert_message_to_dict(m) for m in messages] for message in messages_dict: @@ -1601,11 +1588,9 @@ class BaseChatOpenAI(BaseChatModel): "Token counts for file inputs are not supported. " "Ignoring file inputs." ) - pass else: - raise ValueError( - f"Unrecognized content block type\n\n{val}" - ) + msg = f"Unrecognized content block type\n\n{val}" + raise ValueError(msg) elif not value: continue else: @@ -1627,7 +1612,7 @@ class BaseChatOpenAI(BaseChatModel): self, functions: Sequence[Union[dict[str, Any], type[BaseModel], Callable, BaseTool]], function_call: Optional[ - Union[_FunctionCall, str, Literal["auto", "none"]] + Union[_FunctionCall, str, Literal["auto", "none"]] # noqa: PYI051 ] = None, **kwargs: Any, ) -> Runnable[LanguageModelInput, BaseMessage]: @@ -1652,7 +1637,6 @@ class BaseChatOpenAI(BaseChatModel): **kwargs: Any additional parameters to pass to the :class:`~langchain.runnable.Runnable` constructor. """ - formatted_functions = [convert_to_openai_function(fn) for fn in functions] if function_call is not None: function_call = ( @@ -1662,18 +1646,20 @@ class BaseChatOpenAI(BaseChatModel): else function_call ) if isinstance(function_call, dict) and len(formatted_functions) != 1: - raise ValueError( + msg = ( "When specifying `function_call`, you must provide exactly one " "function." ) + raise ValueError(msg) if ( isinstance(function_call, dict) and formatted_functions[0]["name"] != function_call["name"] ): - raise ValueError( + msg = ( f"Function call {function_call} was specified, but the only " f"provided function was {formatted_functions[0]['name']}." ) + raise ValueError(msg) kwargs = {**kwargs, "function_call": function_call} return super().bind(functions=formatted_functions, **kwargs) @@ -1682,7 +1668,7 @@ class BaseChatOpenAI(BaseChatModel): tools: Sequence[Union[dict[str, Any], type, Callable, BaseTool]], *, tool_choice: Optional[ - Union[dict, str, Literal["auto", "none", "required", "any"], bool] + Union[dict, str, Literal["auto", "none", "required", "any"], bool] # noqa: PYI051 ] = None, strict: Optional[bool] = None, parallel_tool_calls: Optional[bool] = None, @@ -1720,7 +1706,6 @@ class BaseChatOpenAI(BaseChatModel): Support for ``strict`` argument added. """ # noqa: E501 - if parallel_tool_calls is not None: kwargs["parallel_tool_calls"] = parallel_tool_calls formatted_tools = [ @@ -1755,10 +1740,11 @@ class BaseChatOpenAI(BaseChatModel): elif isinstance(tool_choice, dict): pass else: - raise ValueError( + msg = ( f"Unrecognized tool_choice type. Expected str, bool or dict. " f"Received: {tool_choice}" ) + raise ValueError(msg) kwargs["tool_choice"] = tool_choice return super().bind(tools=formatted_tools, **kwargs) @@ -1902,9 +1888,8 @@ class BaseChatOpenAI(BaseChatModel): """ # noqa: E501 if strict is not None and method == "json_mode": - raise ValueError( - "Argument `strict` is not supported with `method`='json_mode'" - ) + msg = "Argument `strict` is not supported with `method`='json_mode'" + raise ValueError(msg) is_pydantic_schema = _is_pydantic_class(schema) if method == "json_schema": @@ -1937,22 +1922,21 @@ class BaseChatOpenAI(BaseChatModel): if method == "function_calling": if schema is None: - raise ValueError( + msg = ( "schema must be specified when method is not 'json_mode'. " "Received None." ) + raise ValueError(msg) tool_name = convert_to_openai_tool(schema)["function"]["name"] bind_kwargs = self._filter_disabled_params( **{ - **dict( - tool_choice=tool_name, - parallel_tool_calls=False, - strict=strict, - ls_structured_output_format={ - "kwargs": {"method": method, "strict": strict}, - "schema": schema, - }, - ), + "tool_choice": tool_name, + "parallel_tool_calls": False, + "strict": strict, + "ls_structured_output_format": { + "kwargs": {"method": method, "strict": strict}, + "schema": schema, + }, **kwargs, } ) @@ -1970,13 +1954,11 @@ class BaseChatOpenAI(BaseChatModel): elif method == "json_mode": llm = self.bind( **{ - **dict( - response_format={"type": "json_object"}, - ls_structured_output_format={ - "kwargs": {"method": method}, - "schema": schema, - }, - ), + "response_format": {"type": "json_object"}, + "ls_structured_output_format": { + "kwargs": {"method": method}, + "schema": schema, + }, **kwargs, } ) @@ -1987,10 +1969,11 @@ class BaseChatOpenAI(BaseChatModel): ) elif method == "json_schema": if schema is None: - raise ValueError( + msg = ( "schema must be specified when method is not 'json_mode'. " "Received None." ) + raise ValueError(msg) response_format = _convert_to_openai_response_format(schema, strict=strict) bind_kwargs = { **dict( @@ -2014,10 +1997,11 @@ class BaseChatOpenAI(BaseChatModel): else: output_parser = JsonOutputParser() else: - raise ValueError( + msg = ( f"Unrecognized method argument. Expected one of 'function_calling' or " f"'json_mode'. Received: '{method}'" ) + raise ValueError(msg) if include_raw: parser_assign = RunnablePassthrough.assign( @@ -2028,8 +2012,7 @@ class BaseChatOpenAI(BaseChatModel): [parser_none], exception_key="parsing_error" ) return RunnableMap(raw=llm) | parser_with_fallback - else: - return llm | output_parser + return llm | output_parser def _filter_disabled_params(self, **kwargs: Any) -> dict[str, Any]: if not self.disabled_params: @@ -2042,8 +2025,7 @@ class BaseChatOpenAI(BaseChatModel): ): continue # Keep param - else: - filtered[k] = v + filtered[k] = v return filtered def _get_generation_chunk_from_completion( @@ -2070,7 +2052,7 @@ class BaseChatOpenAI(BaseChatModel): class ChatOpenAI(BaseChatOpenAI): # type: ignore[override] - """OpenAI chat model integration. + r"""OpenAI chat model integration. .. dropdown:: Setup :open: @@ -2822,6 +2804,7 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override] @property def lc_secrets(self) -> dict[str, str]: + """Mapping of secret environment variables.""" return {"openai_api_key": "OPENAI_API_KEY"} @classmethod @@ -2831,6 +2814,7 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override] @property def lc_attributes(self) -> dict[str, Any]: + """Get the attributes of the langchain object.""" attributes: dict[str, Any] = {} if self.openai_organization: @@ -2882,8 +2866,7 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override] """Route to Chat Completions or Responses API.""" if self._use_responses_api({**kwargs, **self.model_kwargs}): return super()._stream_responses(*args, **kwargs) - else: - return super()._stream(*args, **kwargs) + return super()._stream(*args, **kwargs) async def _astream( self, *args: Any, **kwargs: Any @@ -2905,7 +2888,7 @@ class ChatOpenAI(BaseChatOpenAI): # type: ignore[override] strict: Optional[bool] = None, **kwargs: Any, ) -> Runnable[LanguageModelInput, _DictOrPydantic]: - """Model wrapper that returns outputs formatted to match the given schema. + r"""Model wrapper that returns outputs formatted to match the given schema. Args: schema: The output schema. Can be passed in as: @@ -3306,13 +3289,12 @@ def _url_to_size(image_source: str) -> Optional[tuple[int, int]]: response.raise_for_status() width, height = Image.open(BytesIO(response.content)).size return width, height - elif _is_b64(image_source): + if _is_b64(image_source): _, encoded = image_source.split(",", 1) data = base64.b64decode(encoded) width, height = Image.open(BytesIO(data)).size return width, height - else: - return None + return None def _count_image_tokens(width: int, height: int) -> int: @@ -3328,7 +3310,7 @@ def _is_url(s: str) -> bool: result = urlparse(s) return all([result.scheme, result.netloc]) except Exception as e: - logger.debug(f"Unable to parse URL: {e}") + logger.debug("Unable to parse URL: %s", e) return False @@ -3401,17 +3383,16 @@ def _oai_structured_outputs_parser( if parsed := ai_msg.additional_kwargs.get("parsed"): if isinstance(parsed, dict): return schema(**parsed) - else: - return parsed - elif ai_msg.additional_kwargs.get("refusal"): + return parsed + if ai_msg.additional_kwargs.get("refusal"): raise OpenAIRefusalError(ai_msg.additional_kwargs["refusal"]) - elif ai_msg.tool_calls: + if ai_msg.tool_calls: return None - else: - raise ValueError( - "Structured Output response does not have a 'parsed' field nor a 'refusal' " - f"field. Received message:\n\n{ai_msg}" - ) + msg = ( + "Structured Output response does not have a 'parsed' field nor a 'refusal' " + f"field. Received message:\n\n{ai_msg}" + ) + raise ValueError(msg) class OpenAIRefusalError(Exception): @@ -3508,11 +3489,12 @@ def _use_responses_api(payload: dict) -> bool: def _get_last_messages( messages: Sequence[BaseMessage], ) -> tuple[Sequence[BaseMessage], Optional[str]]: - """ - Return - 1. Every message after the most-recent AIMessage that has a non-empty - ``response_metadata["id"]`` (may be an empty list), - 2. That id. + """Get the last part of the conversation after the most recent AIMessage with an id. + + Return: + 1. Every message after the most-recent AIMessage that has a non-empty + ``response_metadata["id"]`` (may be an empty list), + 2. That id. If the most-recent AIMessage does not have an id (or there is no AIMessage at all) the entire conversation is returned together with ``None``. @@ -3555,13 +3537,14 @@ def _construct_responses_api_payload( if tool["type"] == "image_generation": # Handle partial images (not yet supported) if "partial_images" in tool: - raise NotImplementedError( + msg = ( "Partial image generation is not yet supported " "via the LangChain ChatOpenAI client. Please " "drop the 'partial_images' key from the image_generation " "tool." ) - elif payload.get("stream") and "partial_images" not in tool: + raise NotImplementedError(msg) + if payload.get("stream") and "partial_images" not in tool: # OpenAI requires this parameter be set; we ignore it during # streaming. tool["partial_images"] = 1 @@ -3666,8 +3649,9 @@ def _make_custom_tool_output_from_message(message: ToolMessage) -> Optional[dict def _pop_index_and_sub_index(block: dict) -> dict: - """When streaming, langchain-core uses the ``index`` key to aggregate - text blocks. OpenAI API does not support this key, so we need to remove it. + """When streaming, langchain-core uses ``index`` to aggregate text blocks. + + OpenAI API does not support this key, so we need to remove it. """ new_block = {k: v for k, v in block.items() if k != "index"} if "summary" in new_block and isinstance(new_block["summary"], list): @@ -3836,16 +3820,16 @@ def _construct_responses_api_input(messages: Sequence[BaseMessage]) -> list: def _get_output_text(response: Response) -> str: - """OpenAI SDK deleted response.output_text in 1.99.2""" + """OpenAI SDK deleted response.output_text in 1.99.2.""" if hasattr(response, "output_text"): return response.output_text - texts: list[str] = [] - for output in response.output: - if output.type == "message": - for content in output.content: - if content.type == "output_text": - texts.append(content.text) - + texts = [ + content.text + for output in response.output + if output.type == "message" + for content in output.content + if content.type == "output_text" + ] return "".join(texts) @@ -4067,10 +4051,7 @@ def _convert_responses_chunk_to_generation_chunk( content = [] tool_call_chunks: list = [] additional_kwargs: dict = {} - if metadata: - response_metadata = metadata - else: - response_metadata = {} + response_metadata = metadata or {} usage_metadata = None id = None if chunk.type == "response.output_text.delta": diff --git a/libs/partners/openai/langchain_openai/embeddings/__init__.py b/libs/partners/openai/langchain_openai/embeddings/__init__.py index 3ee80c57ac5..e48ff606f01 100644 --- a/libs/partners/openai/langchain_openai/embeddings/__init__.py +++ b/libs/partners/openai/langchain_openai/embeddings/__init__.py @@ -1,4 +1,6 @@ +"""Module for OpenAI embeddings.""" + from langchain_openai.embeddings.azure import AzureOpenAIEmbeddings from langchain_openai.embeddings.base import OpenAIEmbeddings -__all__ = ["OpenAIEmbeddings", "AzureOpenAIEmbeddings"] +__all__ = ["AzureOpenAIEmbeddings", "OpenAIEmbeddings"] diff --git a/libs/partners/openai/langchain_openai/embeddings/azure.py b/libs/partners/openai/langchain_openai/embeddings/azure.py index 70e50b2dd08..b341e0cea16 100644 --- a/libs/partners/openai/langchain_openai/embeddings/azure.py +++ b/libs/partners/openai/langchain_openai/embeddings/azure.py @@ -20,7 +20,7 @@ class AzureOpenAIEmbeddings(OpenAIEmbeddings): # type: ignore[override] To access AzureOpenAI embedding models you'll need to create an Azure account, get an API key, and install the `langchain-openai` integration package. - You’ll need to have an Azure OpenAI instance deployed. + You'll need to have an Azure OpenAI instance deployed. You can deploy a version on Azure Portal following this [guide](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?pivots=web-portal). @@ -174,22 +174,23 @@ class AzureOpenAIEmbeddings(OpenAIEmbeddings): # type: ignore[override] openai_api_base = self.openai_api_base if openai_api_base and self.validate_base_url: # Only validate openai_api_base if azure_endpoint is not provided - if not self.azure_endpoint: - if "/openai" not in openai_api_base: - self.openai_api_base = cast(str, self.openai_api_base) + "/openai" - raise ValueError( - "As of openai>=1.0.0, Azure endpoints should be specified via " - "the `azure_endpoint` param not `openai_api_base` " - "(or alias `base_url`). " - ) + if not self.azure_endpoint and "/openai" not in openai_api_base: + self.openai_api_base = cast(str, self.openai_api_base) + "/openai" + msg = ( + "As of openai>=1.0.0, Azure endpoints should be specified via " + "the `azure_endpoint` param not `openai_api_base` " + "(or alias `base_url`). " + ) + raise ValueError(msg) if self.deployment: - raise ValueError( + msg = ( "As of openai>=1.0.0, if `deployment` (or alias " "`azure_deployment`) is specified then " "`openai_api_base` (or alias `base_url`) should not be. " "Instead use `deployment` (or alias `azure_deployment`) " "and `azure_endpoint`." ) + raise ValueError(msg) client_params: dict = { "api_version": self.openai_api_version, "azure_endpoint": self.azure_endpoint, diff --git a/libs/partners/openai/langchain_openai/embeddings/base.py b/libs/partners/openai/langchain_openai/embeddings/base.py index 8f3b1d605bb..7416a66a980 100644 --- a/libs/partners/openai/langchain_openai/embeddings/base.py +++ b/libs/partners/openai/langchain_openai/embeddings/base.py @@ -1,3 +1,5 @@ +"""Base classes for OpenAI embeddings.""" + from __future__ import annotations import logging @@ -50,29 +52,25 @@ def _process_batched_chunked_embeddings( embeddings.append(None) continue - elif len(_result) == 1: + if len(_result) == 1: # if only one embedding was produced, use it embeddings.append(_result[0]) continue - else: - # else we need to weighted average - # should be same as - # average = np.average(_result, axis=0, weights=num_tokens_in_batch[i]) - total_weight = sum(num_tokens_in_batch[i]) - average = [ - sum( - val * weight - for val, weight in zip(embedding, num_tokens_in_batch[i]) - ) - / total_weight - for embedding in zip(*_result) - ] + # else we need to weighted average + # should be same as + # average = np.average(_result, axis=0, weights=num_tokens_in_batch[i]) + total_weight = sum(num_tokens_in_batch[i]) + average = [ + sum(val * weight for val, weight in zip(embedding, num_tokens_in_batch[i])) + / total_weight + for embedding in zip(*_result) + ] - # should be same as - # embeddings.append((average / np.linalg.norm(average)).tolist()) - magnitude = sum(val**2 for val in average) ** 0.5 - embeddings.append([val / magnitude for val in average]) + # should be same as + # embeddings.append((average / np.linalg.norm(average)).tolist()) + magnitude = sum(val**2 for val in average) ** 0.5 + embeddings.append([val / magnitude for val in average]) return embeddings @@ -267,7 +265,8 @@ class OpenAIEmbeddings(BaseModel, Embeddings): extra = values.get("model_kwargs", {}) for field_name in list(values): if field_name in extra: - raise ValueError(f"Found {field_name} supplied twice.") + msg = f"Found {field_name} supplied twice." + raise ValueError(msg) if field_name not in all_required_field_names: warnings.warn( f"""WARNING! {field_name} is not default parameter. @@ -278,10 +277,11 @@ class OpenAIEmbeddings(BaseModel, Embeddings): invalid_model_kwargs = all_required_field_names.intersection(extra.keys()) if invalid_model_kwargs: - raise ValueError( + msg = ( f"Parameters {invalid_model_kwargs} should be specified explicitly. " f"Instead they were passed in as part of `model_kwargs` parameter." ) + raise ValueError(msg) values["model_kwargs"] = extra return values @@ -290,9 +290,10 @@ class OpenAIEmbeddings(BaseModel, Embeddings): def validate_environment(self) -> Self: """Validate that api key and python package exists in environment.""" if self.openai_api_type in ("azure", "azure_ad", "azuread"): - raise ValueError( + msg = ( "If you are using Azure, please use the `AzureOpenAIEmbeddings` class." ) + raise ValueError(msg) client_params: dict = { "api_key": ( self.openai_api_key.get_secret_value() if self.openai_api_key else None @@ -309,20 +310,22 @@ class OpenAIEmbeddings(BaseModel, Embeddings): openai_proxy = self.openai_proxy http_client = self.http_client http_async_client = self.http_async_client - raise ValueError( + msg = ( "Cannot specify 'openai_proxy' if one of " "'http_client'/'http_async_client' is already specified. Received:\n" f"{openai_proxy=}\n{http_client=}\n{http_async_client=}" ) + raise ValueError(msg) if not self.client: if self.openai_proxy and not self.http_client: try: import httpx except ImportError as e: - raise ImportError( + msg = ( "Could not import httpx python package. " "Please install it with `pip install httpx`." - ) from e + ) + raise ImportError(msg) from e self.http_client = httpx.Client(proxy=self.openai_proxy) sync_specific = {"http_client": self.http_client} self.client = openai.OpenAI(**client_params, **sync_specific).embeddings # type: ignore[arg-type] @@ -331,10 +334,11 @@ class OpenAIEmbeddings(BaseModel, Embeddings): try: import httpx except ImportError as e: - raise ImportError( + msg = ( "Could not import httpx python package. " "Please install it with `pip install httpx`." - ) from e + ) + raise ImportError(msg) from e self.http_async_client = httpx.AsyncClient(proxy=self.openai_proxy) async_specific = {"http_client": self.http_async_client} self.async_client = openai.AsyncOpenAI( @@ -353,8 +357,7 @@ class OpenAIEmbeddings(BaseModel, Embeddings): def _tokenize( self, texts: list[str], chunk_size: int ) -> tuple[Iterable[int], list[Union[list[int], str]], list[int]]: - """ - Take the input `texts` and `chunk_size` and return 3 iterables as a tuple: + """Take the input `texts` and `chunk_size` and return 3 iterables as a tuple. We have `batches`, where batches are sets of individual texts we want responses from the openai api. The length of a single batch is @@ -382,11 +385,12 @@ class OpenAIEmbeddings(BaseModel, Embeddings): try: from transformers import AutoTokenizer except ImportError: - raise ValueError( + msg = ( "Could not import transformers python package. " "This is needed for OpenAIEmbeddings to work without " "`tiktoken`. Please install it with `pip install transformers`. " ) + raise ValueError(msg) tokenizer = AutoTokenizer.from_pretrained( pretrained_model_name_or_path=model_name @@ -456,8 +460,7 @@ class OpenAIEmbeddings(BaseModel, Embeddings): chunk_size: Optional[int] = None, **kwargs: Any, ) -> list[list[float]]: - """ - Generate length-safe embeddings for a list of texts. + """Generate length-safe embeddings for a list of texts. This method handles tokenization and embedding generation, respecting the set embedding context length and chunk size. It supports both tiktoken @@ -509,8 +512,7 @@ class OpenAIEmbeddings(BaseModel, Embeddings): chunk_size: Optional[int] = None, **kwargs: Any, ) -> list[list[float]]: - """ - Asynchronously generate length-safe embeddings for a list of texts. + """Asynchronously generate length-safe embeddings for a list of texts. This method handles tokenization and asynchronous embedding generation, respecting the set embedding context length and chunk size. It supports both @@ -524,7 +526,6 @@ class OpenAIEmbeddings(BaseModel, Embeddings): Returns: List[List[float]]: A list of embeddings for each input text. """ - _chunk_size = chunk_size or self.chunk_size client_kwargs = {**self._invocation_params, **kwargs} _iter, tokens, indices = await run_in_executor( diff --git a/libs/partners/openai/langchain_openai/llms/__init__.py b/libs/partners/openai/langchain_openai/llms/__init__.py index 39723c1e0c0..494ae9314bc 100644 --- a/libs/partners/openai/langchain_openai/llms/__init__.py +++ b/libs/partners/openai/langchain_openai/llms/__init__.py @@ -1,4 +1,6 @@ +"""Module for OpenAI large language models. Chat models are in `chat_models/`.""" + from langchain_openai.llms.azure import AzureOpenAI from langchain_openai.llms.base import OpenAI -__all__ = ["OpenAI", "AzureOpenAI"] +__all__ = ["AzureOpenAI", "OpenAI"] diff --git a/libs/partners/openai/langchain_openai/llms/azure.py b/libs/partners/openai/langchain_openai/llms/azure.py index 92c2a091e7b..8e4f83f4164 100644 --- a/libs/partners/openai/langchain_openai/llms/azure.py +++ b/libs/partners/openai/langchain_openai/llms/azure.py @@ -1,3 +1,5 @@ +"""Azure OpenAI large language models. Not to be confused with chat models.""" + from __future__ import annotations import logging @@ -101,6 +103,7 @@ class AzureOpenAI(BaseOpenAI): @property def lc_secrets(self) -> dict[str, str]: + """Mapping of secret keys to environment variables.""" return { "openai_api_key": "AZURE_OPENAI_API_KEY", "azure_ad_token": "AZURE_OPENAI_AD_TOKEN", @@ -115,11 +118,14 @@ class AzureOpenAI(BaseOpenAI): def validate_environment(self) -> Self: """Validate that api key and python package exists in environment.""" if self.n < 1: - raise ValueError("n must be at least 1.") + msg = "n must be at least 1." + raise ValueError(msg) if self.streaming and self.n > 1: - raise ValueError("Cannot stream results when n > 1.") + msg = "Cannot stream results when n > 1." + raise ValueError(msg) if self.streaming and self.best_of > 1: - raise ValueError("Cannot stream results when best_of > 1.") + msg = "Cannot stream results when best_of > 1." + raise ValueError(msg) # For backwards compatibility. Before openai v1, no distinction was made # between azure_endpoint and base_url (openai_api_base). openai_api_base = self.openai_api_base @@ -128,19 +134,21 @@ class AzureOpenAI(BaseOpenAI): self.openai_api_base = ( cast(str, self.openai_api_base).rstrip("/") + "/openai" ) - raise ValueError( + msg = ( "As of openai>=1.0.0, Azure endpoints should be specified via " "the `azure_endpoint` param not `openai_api_base` " "(or alias `base_url`)." ) + raise ValueError(msg) if self.deployment_name: - raise ValueError( + msg = ( "As of openai>=1.0.0, if `deployment_name` (or alias " "`azure_deployment`) is specified then " "`openai_api_base` (or alias `base_url`) should not be. " "Instead use `deployment_name` (or alias `azure_deployment`) " "and `azure_endpoint`." ) + raise ValueError(msg) self.deployment_name = None client_params: dict = { "api_version": self.openai_api_version, @@ -187,7 +195,7 @@ class AzureOpenAI(BaseOpenAI): @property def _identifying_params(self) -> Mapping[str, Any]: return { - **{"deployment_name": self.deployment_name}, + "deployment_name": self.deployment_name, **super()._identifying_params, } @@ -214,6 +222,7 @@ class AzureOpenAI(BaseOpenAI): @property def lc_attributes(self) -> dict[str, Any]: + """Attributes relevant to tracing.""" return { "openai_api_type": self.openai_api_type, "openai_api_version": self.openai_api_version, diff --git a/libs/partners/openai/langchain_openai/llms/base.py b/libs/partners/openai/langchain_openai/llms/base.py index defd522c659..ebb167d2c92 100644 --- a/libs/partners/openai/langchain_openai/llms/base.py +++ b/libs/partners/openai/langchain_openai/llms/base.py @@ -1,3 +1,5 @@ +"""Base classes for OpenAI large language models. Chat models are in `chat_models/`.""" + from __future__ import annotations import logging @@ -41,10 +43,10 @@ def _stream_response_to_generation_chunk( return GenerationChunk(text="") return GenerationChunk( text=stream_response["choices"][0]["text"] or "", - generation_info=dict( - finish_reason=stream_response["choices"][0].get("finish_reason", None), - logprobs=stream_response["choices"][0].get("logprobs", None), - ), + generation_info={ + "finish_reason": stream_response["choices"][0].get("finish_reason", None), + "logprobs": stream_response["choices"][0].get("logprobs", None), + }, ) @@ -261,18 +263,20 @@ class BaseOpenAI(BaseLLM): def build_extra(cls, values: dict[str, Any]) -> Any: """Build extra kwargs from additional params that were passed in.""" 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: """Validate that api key and python package exists in environment.""" if self.n < 1: - raise ValueError("n must be at least 1.") + msg = "n must be at least 1." + raise ValueError(msg) if self.streaming and self.n > 1: - raise ValueError("Cannot stream results when n > 1.") + msg = "Cannot stream results when n > 1." + raise ValueError(msg) if self.streaming and self.best_of > 1: - raise ValueError("Cannot stream results when best_of > 1.") + msg = "Cannot stream results when best_of > 1." + raise ValueError(msg) client_params: dict = { "api_key": ( @@ -394,6 +398,7 @@ class BaseOpenAI(BaseLLM): Args: prompts: The prompts to pass into the model. stop: Optional list of stop words to use when generating. + run_manager: Optional callback manager to use for the call. Returns: The full LLM output. @@ -417,7 +422,8 @@ class BaseOpenAI(BaseLLM): for _prompts in sub_prompts: if self.streaming: if len(_prompts) > 1: - raise ValueError("Cannot stream results with multiple prompts.") + msg = "Cannot stream results with multiple prompts." + raise ValueError(msg) generation: Optional[GenerationChunk] = None for chunk in self._stream(_prompts[0], stop, run_manager, **kwargs): @@ -426,7 +432,8 @@ class BaseOpenAI(BaseLLM): else: generation += chunk if generation is None: - raise ValueError("Generation is empty after streaming.") + msg = "Generation is empty after streaming." + raise ValueError(msg) choices.append( { "text": generation.text, @@ -484,7 +491,8 @@ class BaseOpenAI(BaseLLM): for _prompts in sub_prompts: if self.streaming: if len(_prompts) > 1: - raise ValueError("Cannot stream results with multiple prompts.") + msg = "Cannot stream results with multiple prompts." + raise ValueError(msg) generation: Optional[GenerationChunk] = None async for chunk in self._astream( @@ -495,7 +503,8 @@ class BaseOpenAI(BaseLLM): else: generation += chunk if generation is None: - raise ValueError("Generation is empty after streaming.") + msg = "Generation is empty after streaming." + raise ValueError(msg) choices.append( { "text": generation.text, @@ -532,15 +541,13 @@ class BaseOpenAI(BaseLLM): params["stop"] = stop if params["max_tokens"] == -1: if len(prompts) != 1: - raise ValueError( - "max_tokens set to -1 not supported for multiple inputs." - ) + msg = "max_tokens set to -1 not supported for multiple inputs." + raise ValueError(msg) params["max_tokens"] = self.max_tokens_for_prompt(prompts[0]) - sub_prompts = [ + return [ prompts[i : i + self.batch_size] for i in range(0, len(prompts), self.batch_size) ] - return sub_prompts def create_llm_result( self, @@ -560,10 +567,10 @@ class BaseOpenAI(BaseLLM): [ Generation( text=choice["text"], - generation_info=dict( - finish_reason=choice.get("finish_reason"), - logprobs=choice.get("logprobs"), - ), + generation_info={ + "finish_reason": choice.get("finish_reason"), + "logprobs": choice.get("logprobs"), + }, ) for choice in sub_choices ] @@ -581,7 +588,7 @@ class BaseOpenAI(BaseLLM): @property def _identifying_params(self) -> Mapping[str, Any]: """Get the identifying parameters.""" - return {**{"model_name": self.model_name}, **self._default_params} + return {"model_name": self.model_name, **self._default_params} @property def _llm_type(self) -> str: @@ -659,7 +666,7 @@ class BaseOpenAI(BaseLLM): if "ft-" in modelname: modelname = modelname.split(":")[0] - context_size = model_token_mapping.get(modelname, None) + context_size = model_token_mapping.get(modelname) if context_size is None: raise ValueError( @@ -806,14 +813,16 @@ class OpenAI(BaseOpenAI): @property def _invocation_params(self) -> dict[str, Any]: - return {**{"model": self.model_name}, **super()._invocation_params} + return {"model": self.model_name, **super()._invocation_params} @property def lc_secrets(self) -> dict[str, str]: + """Mapping of secret keys to environment variables.""" return {"openai_api_key": "OPENAI_API_KEY"} @property def lc_attributes(self) -> dict[str, Any]: + """LangChain attributes for this class.""" attributes: dict[str, Any] = {} if self.openai_api_base: attributes["openai_api_base"] = self.openai_api_base diff --git a/libs/partners/openai/langchain_openai/output_parsers/__init__.py b/libs/partners/openai/langchain_openai/output_parsers/__init__.py index bf7b62e88f6..8e776af674f 100644 --- a/libs/partners/openai/langchain_openai/output_parsers/__init__.py +++ b/libs/partners/openai/langchain_openai/output_parsers/__init__.py @@ -1,3 +1,5 @@ +"""Output parsers for OpenAI tools.""" + from langchain_core.output_parsers.openai_tools import ( JsonOutputKeyToolsParser, JsonOutputToolsParser, diff --git a/libs/partners/openai/langchain_openai/output_parsers/tools.py b/libs/partners/openai/langchain_openai/output_parsers/tools.py index 57a1a667226..8e776af674f 100644 --- a/libs/partners/openai/langchain_openai/output_parsers/tools.py +++ b/libs/partners/openai/langchain_openai/output_parsers/tools.py @@ -1,7 +1,9 @@ +"""Output parsers for OpenAI tools.""" + from langchain_core.output_parsers.openai_tools import ( JsonOutputKeyToolsParser, JsonOutputToolsParser, PydanticToolsParser, ) -__all__ = ["PydanticToolsParser", "JsonOutputToolsParser", "JsonOutputKeyToolsParser"] +__all__ = ["JsonOutputKeyToolsParser", "JsonOutputToolsParser", "PydanticToolsParser"] diff --git a/libs/partners/openai/langchain_openai/tools/__init__.py b/libs/partners/openai/langchain_openai/tools/__init__.py index 11e5dd9c95a..4f9db9f0880 100644 --- a/libs/partners/openai/langchain_openai/tools/__init__.py +++ b/libs/partners/openai/langchain_openai/tools/__init__.py @@ -1,3 +1,5 @@ +"""Tools package for OpenAI integrations.""" + from langchain_openai.tools.custom_tool import custom_tool __all__ = ["custom_tool"] diff --git a/libs/partners/openai/langchain_openai/tools/custom_tool.py b/libs/partners/openai/langchain_openai/tools/custom_tool.py index eb527083476..af48d39e296 100644 --- a/libs/partners/openai/langchain_openai/tools/custom_tool.py +++ b/libs/partners/openai/langchain_openai/tools/custom_tool.py @@ -1,3 +1,5 @@ +"""Custom tool decorator for OpenAI custom tools.""" + import inspect from collections.abc import Awaitable from typing import Any, Callable diff --git a/libs/partners/openai/pyproject.toml b/libs/partners/openai/pyproject.toml index 667c491a75e..8d062a915a5 100644 --- a/libs/partners/openai/pyproject.toml +++ b/libs/partners/openai/pyproject.toml @@ -66,8 +66,50 @@ target-version = "py39" docstring-code-format = true [tool.ruff.lint] -select = ["E", "F", "I", "T201", "UP", "S"] -ignore = [ "UP007", "UP045" ] +select = ["ALL"] +ignore = [ + "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 + "FIX", # TODOs + "TD", # TODOs + "C901", # Complex functions + "PLR0912", # Too many branches + "PLR0913", # Too many arguments + "PLR0914", # Too many local variables + "PLR0915", # Too many statements + "ARG001", + "RUF001", + "ERA001", + "PLR0911", + + # TODO + "PLR2004", # Comparison to magic number + "ANN401", + "ARG002", + "BLE001", + "TC", + "PLC0415", + "PT011", + "PT013", + "TRY", + "PLW", + "PLE", + "FBT", + "A001", + "B028", + "YTT203", + "RUF012", + "B904", +] +unfixable = ["B028"] # People should intentionally tune the stacklevel + +[tool.ruff.lint.pydocstyle] +convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.coverage.run] omit = ["tests/*"] @@ -88,4 +130,17 @@ filterwarnings = [ "tests/**/*.py" = [ "S101", # Tests need assertions "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "SLF001", # Private member access in tests + "D", # Docstring checks in tests + + # TODO + "B018", + "PGH003", + "PERF401", + "PT017", + "RUF012", + "B017", +] +"scripts/*.py" = [ + "INP001", # Not a package ] diff --git a/libs/partners/openai/scripts/check_imports.py b/libs/partners/openai/scripts/check_imports.py index 58a460c1493..698173fb7bd 100644 --- a/libs/partners/openai/scripts/check_imports.py +++ b/libs/partners/openai/scripts/check_imports.py @@ -1,3 +1,5 @@ +"""Script to check for import errors in specified Python files.""" + import sys import traceback from importlib.machinery import SourceFileLoader diff --git a/libs/partners/openai/tests/conftest.py b/libs/partners/openai/tests/conftest.py index 064431d3b69..77c231f915d 100644 --- a/libs/partners/openai/tests/conftest.py +++ b/libs/partners/openai/tests/conftest.py @@ -2,7 +2,9 @@ from typing import Any import pytest from langchain_tests.conftest import CustomPersister, CustomSerializer -from langchain_tests.conftest import _base_vcr_config as _base_vcr_config +from langchain_tests.conftest import ( + _base_vcr_config as _base_vcr_config, # noqa: PLC0414 +) from vcr import VCR # type: ignore[import-untyped] _EXTRA_HEADERS = [ diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_azure.py b/libs/partners/openai/tests/integration_tests/chat_models/test_azure.py index e9228df0730..4164c678684 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_azure.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_azure.py @@ -1,5 +1,7 @@ """Test AzureChatOpenAI wrapper.""" +from __future__ import annotations + import json import os from typing import Any, Optional @@ -224,7 +226,7 @@ async def test_openai_ainvoke(llm: AzureChatOpenAI) -> None: def test_openai_invoke(llm: AzureChatOpenAI) -> None: """Test invoke tokens from AzureChatOpenAI.""" - 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 result.response_metadata.get("model_name") is not None diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py index e0064e73e21..b64f9369d85 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_base.py @@ -1,5 +1,7 @@ """Test ChatOpenAI chat model.""" +from __future__ import annotations + import base64 import json from collections.abc import AsyncIterator @@ -224,7 +226,7 @@ def test_openai_invoke() -> None: max_retries=3, # Add retries for 503 capacity errors ) - result = llm.invoke("Hello", config=dict(tags=["foo"])) + result = llm.invoke("Hello", config={"tags": ["foo"]}) assert isinstance(result.content, str) # assert no response headers if include_response_headers is not set @@ -256,11 +258,12 @@ def test_stream() -> None: if chunk.response_metadata: chunks_with_response_metadata += 1 if chunks_with_token_counts != 1 or chunks_with_response_metadata != 1: - raise AssertionError( + msg = ( "Expected exactly one chunk with metadata. " "AIMessageChunk aggregation can add these metadata. Check that " "this is behaving properly." ) + raise AssertionError(msg) assert isinstance(aggregate, AIMessageChunk) assert aggregate.usage_metadata is not None assert aggregate.usage_metadata["input_tokens"] > 0 @@ -285,20 +288,22 @@ async def test_astream() -> None: chunks_with_response_metadata += 1 assert isinstance(full, AIMessageChunk) if chunks_with_response_metadata != 1: - raise AssertionError( + msg = ( "Expected exactly one chunk with metadata. " "AIMessageChunk aggregation can add these metadata. Check that " "this is behaving properly." ) + raise AssertionError(msg) assert full.response_metadata.get("finish_reason") is not None assert full.response_metadata.get("model_name") is not None if expect_usage: if chunks_with_token_counts != 1: - raise AssertionError( + msg = ( "Expected exactly one chunk with token counts. " "AIMessageChunk aggregation adds counts. Check that " "this is behaving properly." ) + raise AssertionError(msg) assert full.usage_metadata is not None assert full.usage_metadata["input_tokens"] > 0 assert full.usage_metadata["output_tokens"] > 0 @@ -483,7 +488,8 @@ def test_manual_tool_call_msg(use_responses_api: bool) -> None: output: AIMessage = cast(AIMessage, llm_with_tool.invoke(msgs)) assert output.content # Should not have called the tool again. - assert not output.tool_calls and not output.invalid_tool_calls + assert not output.tool_calls + assert not output.invalid_tool_calls # OpenAI should error when tool call id doesn't match across AIMessage and # ToolMessage @@ -556,7 +562,7 @@ def test_openai_proxy() -> None: chat_openai = ChatOpenAI(openai_proxy="http://localhost:8080") mounts = chat_openai.client._client._client._mounts assert len(mounts) == 1 - for key, value in mounts.items(): + for value in mounts.values(): proxy = value._pool._proxy_url.origin assert proxy.scheme == b"http" assert proxy.host == b"localhost" @@ -564,7 +570,7 @@ def test_openai_proxy() -> None: async_client_mounts = chat_openai.async_client._client._client._mounts assert len(async_client_mounts) == 1 - for key, value in async_client_mounts.items(): + for value in async_client_mounts.values(): proxy = value._pool._proxy_url.origin assert proxy.scheme == b"http" assert proxy.host == b"localhost" @@ -690,7 +696,7 @@ def test_tool_calling_strict(use_responses_api: bool) -> None: Responses API appears to have fewer constraints on schema when strict=True. """ - class magic_function_notrequired_arg(BaseModel): + class magic_function_notrequired_arg(BaseModel): # noqa: N801 """Applies a magic function to an input.""" input: Optional[int] = Field(default=None) @@ -1166,12 +1172,13 @@ class BadModel(BaseModel): @classmethod def validate_response(cls, v: str) -> str: if v != "bad": - raise ValueError('response must be exactly "bad"') + msg = 'response must be exactly "bad"' + raise ValueError(msg) return v # VCR can't handle parameterized tests -@pytest.mark.vcr() +@pytest.mark.vcr def test_schema_parsing_failures() -> None: llm = ChatOpenAI(model="gpt-5-nano", use_responses_api=False) try: @@ -1179,11 +1186,11 @@ def test_schema_parsing_failures() -> None: except Exception as e: assert e.response is not None # type: ignore[attr-defined] else: - assert False + raise AssertionError # VCR can't handle parameterized tests -@pytest.mark.vcr() +@pytest.mark.vcr def test_schema_parsing_failures_responses_api() -> None: llm = ChatOpenAI(model="gpt-5-nano", use_responses_api=True) try: @@ -1191,11 +1198,11 @@ def test_schema_parsing_failures_responses_api() -> None: except Exception as e: assert e.response is not None # type: ignore[attr-defined] else: - assert False + raise AssertionError # VCR can't handle parameterized tests -@pytest.mark.vcr() +@pytest.mark.vcr async def test_schema_parsing_failures_async() -> None: llm = ChatOpenAI(model="gpt-5-nano", use_responses_api=False) try: @@ -1203,11 +1210,11 @@ async def test_schema_parsing_failures_async() -> None: except Exception as e: assert e.response is not None # type: ignore[attr-defined] else: - assert False + raise AssertionError # VCR can't handle parameterized tests -@pytest.mark.vcr() +@pytest.mark.vcr async def test_schema_parsing_failures_responses_api_async() -> None: llm = ChatOpenAI(model="gpt-5-nano", use_responses_api=True) try: @@ -1215,4 +1222,4 @@ async def test_schema_parsing_failures_responses_api_async() -> None: except Exception as e: assert e.response is not None # type: ignore[attr-defined] else: - assert False + raise AssertionError diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_base_standard.py b/libs/partners/openai/tests/integration_tests/chat_models/test_base_standard.py index b51efff1ceb..d7565ab17b5 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_base_standard.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_base_standard.py @@ -62,7 +62,7 @@ class TestOpenAIStandard(ChatModelIntegrationTests): return True def invoke_with_cache_read_input(self, *, stream: bool = False) -> AIMessage: - with open(REPO_ROOT_DIR / "README.md") as f: + with Path.open(REPO_ROOT_DIR / "README.md") as f: readme = f.read() input_ = f"""What's langchain? Here's the langchain README: @@ -129,11 +129,10 @@ def _invoke(llm: ChatOpenAI, input_: str, stream: bool) -> AIMessage: for chunk in llm.stream(input_): full = full + chunk if full else chunk # type: ignore[operator] return cast(AIMessage, full) - else: - return cast(AIMessage, llm.invoke(input_)) + return cast(AIMessage, llm.invoke(input_)) -@pytest.mark.skip() # Test either finishes in 5 seconds or 5 minutes. +@pytest.mark.skip # Test either finishes in 5 seconds or 5 minutes. def test_audio_model() -> None: class AudioModelTests(ChatModelIntegrationTests): @property diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_responses_api.py b/libs/partners/openai/tests/integration_tests/chat_models/test_responses_api.py index 1430d7008b5..e42c7856a36 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_responses_api.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_responses_api.py @@ -1,5 +1,7 @@ """Test Responses API usage.""" +from __future__ import annotations + import json import os from typing import Annotated, Any, Literal, Optional, cast @@ -217,7 +219,8 @@ def test_parsed_dict_schema(schema: Any) -> None: response = llm.invoke("how are ya", response_format=schema) parsed = json.loads(response.text()) assert parsed == response.additional_kwargs["parsed"] - assert parsed["response"] and isinstance(parsed["response"], str) + assert parsed["response"] + assert isinstance(parsed["response"], str) # Test stream full: Optional[BaseMessageChunk] = None @@ -227,7 +230,8 @@ def test_parsed_dict_schema(schema: Any) -> None: assert isinstance(full, AIMessageChunk) parsed = json.loads(full.text()) assert parsed == full.additional_kwargs["parsed"] - assert parsed["response"] and isinstance(parsed["response"], str) + assert parsed["response"] + assert isinstance(parsed["response"], str) def test_parsed_strict() -> None: @@ -262,7 +266,8 @@ async def test_parsed_dict_schema_async(schema: Any) -> None: response = await llm.ainvoke("how are ya", response_format=schema) parsed = json.loads(response.text()) assert parsed == response.additional_kwargs["parsed"] - assert parsed["response"] and isinstance(parsed["response"], str) + assert parsed["response"] + assert isinstance(parsed["response"], str) # Test stream full: Optional[BaseMessageChunk] = None @@ -272,7 +277,8 @@ async def test_parsed_dict_schema_async(schema: Any) -> None: assert isinstance(full, AIMessageChunk) parsed = json.loads(full.text()) assert parsed == full.additional_kwargs["parsed"] - assert parsed["response"] and isinstance(parsed["response"], str) + assert parsed["response"] + assert isinstance(parsed["response"], str) def test_function_calling_and_structured_output() -> None: @@ -548,7 +554,7 @@ def test_mcp_builtin_zdr() -> None: _ = llm_with_tools.invoke([input_message, full, approval_message]) -@pytest.mark.vcr() +@pytest.mark.vcr def test_image_generation_streaming() -> None: """Test image generation streaming.""" llm = ChatOpenAI(model="gpt-4.1", use_responses_api=True) @@ -602,7 +608,7 @@ def test_image_generation_streaming() -> None: assert set(tool_output.keys()).issubset(expected_keys) -@pytest.mark.vcr() +@pytest.mark.vcr def test_image_generation_multi_turn() -> None: """Test multi-turn editing of image generation by passing in history.""" # Test multi-turn @@ -689,7 +695,7 @@ def test_verbosity_parameter() -> None: assert response.content -@pytest.mark.vcr() +@pytest.mark.vcr def test_custom_tool() -> None: @custom_tool def execute_code(code: str) -> str: diff --git a/libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py b/libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py index d06ed1e40f5..e4f1d182941 100644 --- a/libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py +++ b/libs/partners/openai/tests/integration_tests/chat_models/test_responses_standard.py @@ -27,7 +27,7 @@ class TestOpenAIResponses(TestOpenAIStandard): super().test_stop_sequence(model) def invoke_with_cache_read_input(self, *, stream: bool = False) -> AIMessage: - with open(REPO_ROOT_DIR / "README.md") as f: + with Path.open(REPO_ROOT_DIR / "README.md") as f: readme = f.read() input_ = f"""What's langchain? Here's the langchain README: @@ -55,5 +55,4 @@ def _invoke(llm: ChatOpenAI, input_: str, stream: bool) -> AIMessage: for chunk in llm.stream(input_): full = full + chunk if full else chunk # type: ignore[operator] return cast(AIMessage, full) - else: - return cast(AIMessage, llm.invoke(input_)) + return cast(AIMessage, llm.invoke(input_)) diff --git a/libs/partners/openai/tests/integration_tests/embeddings/test_azure.py b/libs/partners/openai/tests/integration_tests/embeddings/test_azure.py index 18644ee66aa..f7b88b93fba 100644 --- a/libs/partners/openai/tests/integration_tests/embeddings/test_azure.py +++ b/libs/partners/openai/tests/integration_tests/embeddings/test_azure.py @@ -63,7 +63,7 @@ def test_azure_openai_embedding_documents_chunk_size() -> None: # Max 2048 chunks per batch on Azure OpenAI embeddings assert embedding.chunk_size == 2048 assert len(output) == 20 - assert all([len(out) == 1536 for out in output]) + assert all(len(out) == 1536 for out in output) @pytest.mark.scheduled diff --git a/libs/partners/openai/tests/integration_tests/llms/test_azure.py b/libs/partners/openai/tests/integration_tests/llms/test_azure.py index 4d601c0f2ae..f1b84ed4392 100644 --- a/libs/partners/openai/tests/integration_tests/llms/test_azure.py +++ b/libs/partners/openai/tests/integration_tests/llms/test_azure.py @@ -98,7 +98,7 @@ async def test_openai_ainvoke(llm: AzureOpenAI) -> None: @pytest.mark.scheduled def test_openai_invoke(llm: AzureOpenAI) -> None: """Test streaming tokens from AzureOpenAI.""" - result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"])) + result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]}) assert isinstance(result, str) diff --git a/libs/partners/openai/tests/integration_tests/llms/test_base.py b/libs/partners/openai/tests/integration_tests/llms/test_base.py index 703391a50ac..30a7ae77178 100644 --- a/libs/partners/openai/tests/integration_tests/llms/test_base.py +++ b/libs/partners/openai/tests/integration_tests/llms/test_base.py @@ -67,7 +67,7 @@ def test_invoke() -> None: """Test invoke tokens from OpenAI.""" llm = OpenAI() - result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"])) + result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]}) assert isinstance(result, str) @@ -164,7 +164,7 @@ def test_openai_invoke() -> None: """Test streaming tokens from OpenAI.""" llm = OpenAI(max_tokens=10) - result = llm.invoke("I'm Pickle Rick", config=dict(tags=["foo"])) + result = llm.invoke("I'm Pickle Rick", config={"tags": ["foo"]}) assert isinstance(result, str) diff --git a/libs/partners/openai/tests/integration_tests/test_compile.py b/libs/partners/openai/tests/integration_tests/test_compile.py index 33ecccdfa0f..f315e45f521 100644 --- a/libs/partners/openai/tests/integration_tests/test_compile.py +++ b/libs/partners/openai/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/openai/tests/unit_tests/chat_models/test_base.py b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py index 01a1e1cae98..1a5fcefa4e5 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_base.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_base.py @@ -1,5 +1,7 @@ """Test OpenAI Chat API wrapper.""" +from __future__ import annotations + import json from functools import partial from types import TracebackType @@ -46,7 +48,7 @@ from openai.types.responses.response_usage import ( OutputTokensDetails, ) from pydantic import BaseModel, Field, SecretStr -from typing_extensions import TypedDict +from typing_extensions import Self, TypedDict from langchain_openai import ChatOpenAI from langchain_openai.chat_models._compat import ( @@ -231,7 +233,7 @@ def test__convert_dict_to_message_tool_call() -> None: "type": "function", }, ] - raw_tool_calls = list(sorted(raw_tool_calls, key=lambda x: x["id"])) + raw_tool_calls = sorted(raw_tool_calls, key=lambda x: x["id"]) message = {"role": "assistant", "content": None, "tool_calls": raw_tool_calls} result = _convert_dict_to_message(message) expected_output = AIMessage( @@ -262,19 +264,19 @@ def test__convert_dict_to_message_tool_call() -> None: ) assert result == expected_output reverted_message_dict = _convert_message_to_dict(expected_output) - reverted_message_dict["tool_calls"] = list( - sorted(reverted_message_dict["tool_calls"], key=lambda x: x["id"]) + reverted_message_dict["tool_calls"] = sorted( + reverted_message_dict["tool_calls"], key=lambda x: x["id"] ) assert reverted_message_dict == message class MockAsyncContextManager: - def __init__(self, chunk_list: list): + def __init__(self, chunk_list: list) -> None: self.current_chunk = 0 self.chunk_list = chunk_list self.chunk_num = len(chunk_list) - async def __aenter__(self) -> "MockAsyncContextManager": + async def __aenter__(self) -> Self: return self async def __aexit__( @@ -285,7 +287,7 @@ class MockAsyncContextManager: ) -> None: pass - def __aiter__(self) -> "MockAsyncContextManager": + def __aiter__(self) -> MockAsyncContextManager: return self async def __anext__(self) -> dict: @@ -293,17 +295,16 @@ class MockAsyncContextManager: chunk = self.chunk_list[self.current_chunk] self.current_chunk += 1 return chunk - else: - raise StopAsyncIteration + raise StopAsyncIteration class MockSyncContextManager: - def __init__(self, chunk_list: list): + def __init__(self, chunk_list: list) -> None: self.current_chunk = 0 self.chunk_list = chunk_list self.chunk_num = len(chunk_list) - def __enter__(self) -> "MockSyncContextManager": + def __enter__(self) -> Self: return self def __exit__( @@ -314,7 +315,7 @@ class MockSyncContextManager: ) -> None: pass - def __iter__(self) -> "MockSyncContextManager": + def __iter__(self) -> MockSyncContextManager: return self def __next__(self) -> dict: @@ -322,8 +323,7 @@ class MockSyncContextManager: chunk = self.chunk_list[self.current_chunk] self.current_chunk += 1 return chunk - else: - raise StopIteration + raise StopIteration GLM4_STREAM_META = """{"id":"20240722102053e7277a4f94e848248ff9588ed37fb6e6","created":1721614853,"model":"glm-4","choices":[{"index":0,"delta":{"role":"assistant","content":"\u4eba\u5de5\u667a\u80fd"}}]} @@ -630,7 +630,6 @@ async def test_openai_ainvoke(mock_async_client: AsyncMock) -> None: ) def test__get_encoding_model(model: str) -> None: ChatOpenAI(model=model)._get_encoding_model() - return def test_openai_invoke_name(mock_client: MagicMock) -> None: @@ -729,7 +728,7 @@ def test_format_message_content() -> None: "input": {"location": "San Francisco, CA", "unit": "celsius"}, }, ] - assert [{"type": "text", "text": "hello"}] == _format_message_content(content) + assert _format_message_content(content) == [{"type": "text", "text": "hello"}] # Standard multi-modal inputs content = [{"type": "image", "source_type": "url", "url": "https://..."}] @@ -899,9 +898,10 @@ def test_get_num_tokens_from_messages() -> None: ] ) ] + actual = 0 with pytest.warns(match="file inputs are not supported"): actual = llm.get_num_tokens_from_messages(messages) - assert actual == 13 + assert actual == 13 class Foo(BaseModel): @@ -1592,7 +1592,7 @@ def test__construct_lc_result_from_responses_api_complex_response() -> None: arguments='{"location": "New York"}', ), ], - metadata=dict(key1="value1", key2="value2"), + metadata={"key1": "value1", "key2": "value2"}, incomplete_details=IncompleteDetails(reason="max_output_tokens"), status="completed", user="user_123", @@ -2310,7 +2310,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}) diff --git a/libs/partners/openai/tests/unit_tests/chat_models/test_responses_stream.py b/libs/partners/openai/tests/unit_tests/chat_models/test_responses_stream.py index eca5ee1c255..6ad17628bb7 100644 --- a/libs/partners/openai/tests/unit_tests/chat_models/test_responses_stream.py +++ b/libs/partners/openai/tests/unit_tests/chat_models/test_responses_stream.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import Any, Optional from unittest.mock import MagicMock, patch @@ -614,10 +616,9 @@ def _strip_none(obj: Any) -> Any: """Recursively strip None values from dictionaries and lists.""" if isinstance(obj, dict): return {k: _strip_none(v) for k, v in obj.items() if v is not None} - elif isinstance(obj, list): + if isinstance(obj, list): return [_strip_none(v) for v in obj] - else: - return obj + return obj def test_responses_stream() -> None: diff --git a/libs/partners/openai/tests/unit_tests/fake/callbacks.py b/libs/partners/openai/tests/unit_tests/fake/callbacks.py index da3fa0664a0..c17cc1f6acf 100644 --- a/libs/partners/openai/tests/unit_tests/fake/callbacks.py +++ b/libs/partners/openai/tests/unit_tests/fake/callbacks.py @@ -1,5 +1,7 @@ """A fake callback handler for testing purposes.""" +from __future__ import annotations + from itertools import chain from typing import Any, Optional, Union from uuid import UUID @@ -188,7 +190,7 @@ class FakeCallbackHandler(BaseCallbackHandler, BaseFakeCallbackHandlerMixin): def on_retriever_error(self, *args: Any, **kwargs: Any) -> Any: self.on_retriever_error_common() - def __deepcopy__(self, memo: dict) -> "FakeCallbackHandler": # type: ignore[override] + def __deepcopy__(self, memo: dict) -> FakeCallbackHandler: # type: ignore[override] return self @@ -266,5 +268,5 @@ class FakeAsyncCallbackHandler(AsyncCallbackHandler, BaseFakeCallbackHandlerMixi async def on_text(self, *args: Any, **kwargs: Any) -> None: self.on_text_common() - def __deepcopy__(self, memo: dict) -> "FakeAsyncCallbackHandler": # type: ignore[override] + def __deepcopy__(self, memo: dict) -> FakeAsyncCallbackHandler: # type: ignore[override] return self diff --git a/libs/partners/openai/tests/unit_tests/llms/test_base.py b/libs/partners/openai/tests/unit_tests/llms/test_base.py index 404c49a4913..f0b027f9b7b 100644 --- a/libs/partners/openai/tests/unit_tests/llms/test_base.py +++ b/libs/partners/openai/tests/unit_tests/llms/test_base.py @@ -65,7 +65,6 @@ def mock_completion() -> dict: @pytest.mark.parametrize("model", ["gpt-3.5-turbo-instruct"]) def test_get_token_ids(model: str) -> None: OpenAI(model=model).get_token_ids("foo") - return def test_custom_token_counting() -> None: diff --git a/libs/partners/openai/tests/unit_tests/test_tools.py b/libs/partners/openai/tests/unit_tests/test_tools.py index 63b097e6248..07a3b86d412 100644 --- a/libs/partners/openai/tests/unit_tests/test_tools.py +++ b/libs/partners/openai/tests/unit_tests/test_tools.py @@ -35,7 +35,6 @@ def test_custom_tool() -> None: @custom_tool(format={"type": "grammar", "syntax": "lark", "definition": "..."}) def another_tool(x: str) -> None: """Do thing.""" - pass llm = ChatOpenAI( model="gpt-4.1", use_responses_api=True, output_version="responses/v1" diff --git a/libs/partners/perplexity/pyproject.toml b/libs/partners/perplexity/pyproject.toml index 80b589fbc20..0d6b47f36e5 100644 --- a/libs/partners/perplexity/pyproject.toml +++ b/libs/partners/perplexity/pyproject.toml @@ -64,6 +64,10 @@ docstring-code-format = true select = ["E", "F", "I", "T201", "UP", "S"] ignore = [ "UP007", "UP045"] +[tool.ruff.lint.pydocstyle] +convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters + [tool.coverage.run] omit = ["tests/*"] diff --git a/libs/partners/prompty/pyproject.toml b/libs/partners/prompty/pyproject.toml index 8fe95ddb224..83eb4595c92 100644 --- a/libs/partners/prompty/pyproject.toml +++ b/libs/partners/prompty/pyproject.toml @@ -60,6 +60,10 @@ docstring-code-format = true select = ["E", "F", "I", "T201", "UP", "S"] ignore = [ "UP007", "UP045" ] +[tool.ruff.lint.pydocstyle] +convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters + [tool.mypy] disallow_untyped_defs = "True" diff --git a/libs/partners/qdrant/langchain_qdrant/_utils.py b/libs/partners/qdrant/langchain_qdrant/_utils.py index 91fc77442ad..0c77b939342 100644 --- a/libs/partners/qdrant/langchain_qdrant/_utils.py +++ b/libs/partners/qdrant/langchain_qdrant/_utils.py @@ -39,30 +39,30 @@ def maximal_marginal_relevance( return idxs -def cosine_similarity(X: Matrix, Y: Matrix) -> np.ndarray: +def cosine_similarity(X: Matrix, Y: Matrix) -> np.ndarray: # noqa: N803 """Row-wise cosine similarity between two equal-width matrices.""" if len(X) == 0 or len(Y) == 0: return np.array([]) - X = np.array(X) - Y = np.array(Y) - if X.shape[1] != Y.shape[1]: + x = np.array(X) + y = np.array(Y) + if x.shape[1] != y.shape[1]: msg = ( - f"Number of columns in X and Y must be the same. X has shape {X.shape} " - f"and Y has shape {Y.shape}." + f"Number of columns in X and Y must be the same. X has shape {x.shape} " + f"and Y has shape {y.shape}." ) raise ValueError(msg) try: - import simsimd as simd + import simsimd as simd # noqa: PLC0415 - X = np.array(X, dtype=np.float32) - Y = np.array(Y, dtype=np.float32) - return 1 - np.array(simd.cdist(X, Y, metric="cosine")) + x = np.array(x, dtype=np.float32) + y = np.array(y, dtype=np.float32) + return 1 - np.array(simd.cdist(x, y, metric="cosine")) except ImportError: - X_norm = np.linalg.norm(X, axis=1) - Y_norm = np.linalg.norm(Y, axis=1) + x_norm = np.linalg.norm(x, axis=1) + y_norm = np.linalg.norm(y, axis=1) # Ignore divide by zero errors run time warnings as those are handled below. with np.errstate(divide="ignore", invalid="ignore"): - similarity = np.dot(X, Y.T) / np.outer(X_norm, Y_norm) + similarity = np.dot(x, y.T) / np.outer(x_norm, y_norm) similarity[np.isnan(similarity) | np.isinf(similarity)] = 0.0 return similarity diff --git a/libs/partners/qdrant/langchain_qdrant/fastembed_sparse.py b/libs/partners/qdrant/langchain_qdrant/fastembed_sparse.py index c7b5fa3b140..2f855a23728 100644 --- a/libs/partners/qdrant/langchain_qdrant/fastembed_sparse.py +++ b/libs/partners/qdrant/langchain_qdrant/fastembed_sparse.py @@ -1,10 +1,12 @@ from __future__ import annotations -from collections.abc import Sequence -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional from langchain_qdrant.sparse_embeddings import SparseEmbeddings, SparseVector +if TYPE_CHECKING: + from collections.abc import Sequence + class FastEmbedSparse(SparseEmbeddings): """An interface for sparse embedding models to use with Qdrant.""" @@ -45,7 +47,7 @@ class FastEmbedSparse(SparseEmbeddings): """ try: - from fastembed import ( # type: ignore[import-not-found] + from fastembed import ( # type: ignore[import-not-found] # noqa: PLC0415 SparseTextEmbedding, ) except ImportError as err: diff --git a/libs/partners/qdrant/langchain_qdrant/qdrant.py b/libs/partners/qdrant/langchain_qdrant/qdrant.py index 4a41cbee1d6..70c3c8e6a6c 100644 --- a/libs/partners/qdrant/langchain_qdrant/qdrant.py +++ b/libs/partners/qdrant/langchain_qdrant/qdrant.py @@ -1,11 +1,11 @@ from __future__ import annotations import uuid -from collections.abc import Generator, Iterable, Sequence from enum import Enum from itertools import islice from operator import itemgetter from typing import ( + TYPE_CHECKING, Any, Callable, Optional, @@ -19,7 +19,11 @@ from langchain_core.vectorstores import VectorStore from qdrant_client import QdrantClient, models from langchain_qdrant._utils import maximal_marginal_relevance -from langchain_qdrant.sparse_embeddings import SparseEmbeddings + +if TYPE_CHECKING: + from collections.abc import Generator, Iterable, Sequence + + from langchain_qdrant.sparse_embeddings import SparseEmbeddings class QdrantVectorStoreError(Exception): @@ -27,6 +31,8 @@ class QdrantVectorStoreError(Exception): class RetrievalMode(str, Enum): + """Modes for retrieving vectors from Qdrant.""" + DENSE = "dense" SPARSE = "sparse" HYBRID = "hybrid" @@ -222,7 +228,7 @@ class QdrantVectorStore(VectorStore): sparse_vector_name: str = SPARSE_VECTOR_NAME, validate_embeddings: bool = True, # noqa: FBT001, FBT002 validate_collection_config: bool = True, # noqa: FBT001, FBT002 - ): + ) -> None: """Initialize a new instance of `QdrantVectorStore`. Example: @@ -295,10 +301,9 @@ class QdrantVectorStore(VectorStore): if self.retrieval_mode == RetrievalMode.SPARSE: # SPARSE mode: no dense embeddings, so no embeddings class name in tags pass - else: - # DENSE/HYBRID modes: include embeddings class name if available - if self.embeddings is not None: - tags.append(self.embeddings.__class__.__name__) + # DENSE/HYBRID modes: include embeddings class name if available + elif self.embeddings is not None: + tags.append(self.embeddings.__class__.__name__) return tags @@ -460,13 +465,11 @@ class QdrantVectorStore(VectorStore): validate_collection_config: bool = True, # noqa: FBT001, FBT002 **kwargs: Any, ) -> QdrantVectorStore: - """Construct an instance of ``QdrantVectorStore`` from an existing collection - without adding any data. + """Construct ``QdrantVectorStore`` from existing collection without adding data. Returns: QdrantVectorStore: A new instance of ``QdrantVectorStore``. - - """ # noqa: D205 + """ client = QdrantClient( location=location, url=url, @@ -1236,7 +1239,7 @@ class QdrantVectorStore(VectorStore): vector_size = len(dense_embeddings) else: msg = "Invalid `embeddings` type." - raise ValueError(msg) + raise TypeError(msg) if vector_config.size != vector_size: msg = ( diff --git a/libs/partners/qdrant/langchain_qdrant/vectorstores.py b/libs/partners/qdrant/langchain_qdrant/vectorstores.py index b642edd2f65..81b44862e2b 100644 --- a/libs/partners/qdrant/langchain_qdrant/vectorstores.py +++ b/libs/partners/qdrant/langchain_qdrant/vectorstores.py @@ -4,7 +4,6 @@ import functools import os import uuid import warnings -from collections.abc import AsyncGenerator, Generator, Iterable, Sequence from itertools import islice from operator import itemgetter from typing import TYPE_CHECKING, Any, Callable, Optional, Union @@ -22,11 +21,13 @@ from qdrant_client.local.async_qdrant_local import AsyncQdrantLocal from langchain_qdrant._utils import maximal_marginal_relevance if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Generator, Iterable, Sequence + DictFilter = dict[str, Union[str, int, bool, dict, list]] MetadataFilter = Union[DictFilter, models.Filter] -class QdrantException(Exception): +class QdrantException(Exception): # noqa: N818 """`Qdrant` related exceptions.""" @@ -85,14 +86,14 @@ class Qdrant(VectorStore): vector_name: Optional[str] = VECTOR_NAME, async_client: Optional[Any] = None, embedding_function: Optional[Callable] = None, # deprecated - ): + ) -> None: """Initialize with necessary components.""" if not isinstance(client, QdrantClient): msg = ( f"client should be an instance of qdrant_client.QdrantClient, " f"got {type(client)}" ) - raise ValueError(msg) + raise TypeError(msg) if async_client is not None and not isinstance(async_client, AsyncQdrantClient): msg = ( diff --git a/libs/partners/qdrant/pyproject.toml b/libs/partners/qdrant/pyproject.toml index 27e06fff359..63efc76afcd 100644 --- a/libs/partners/qdrant/pyproject.toml +++ b/libs/partners/qdrant/pyproject.toml @@ -56,53 +56,8 @@ target-version = "py39" docstring-code-format = true [tool.ruff.lint] -select = [ - "A", # flake8-builtins - "B", # flake8-bugbear - "ASYNC", # flake8-async - "C4", # flake8-comprehensions - "COM", # flake8-commas - "D", # pydocstyle - "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 -] +select = ["ALL"] ignore = [ - "D100", # pydocstyle: Missing docstring in public module - "D101", # pydocstyle: Missing docstring in public class - "D102", # pydocstyle: Missing docstring in public method - "D103", # pydocstyle: Missing docstring in public function - "D104", # pydocstyle: Missing docstring in public package - "D105", # pydocstyle: Missing docstring in magic method - "D107", # pydocstyle: Missing docstring in __init__ - "D203", # Messes with the formatter - "D213", # pydocstyle: Multi-line docstring summary should start at the second line (incompatible with D212) - "D407", # pydocstyle: Missing-dashed-underline-after-section "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful @@ -111,11 +66,21 @@ ignore = [ "SLF001", # Private member access "UP007", # pyupgrade: non-pep604-annotation-union "UP045", # pyupgrade: non-pep604-annotation-optional + "PLR0913", # Function has too many arguments + "C901", # Complex functions + + # TODO" + "ANN401", + "ARG002", + "D100", + "D102", + "D104", ] unfixable = ["B028"] # People should intentionally tune the stacklevel [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.mypy] disallow_untyped_defs = true @@ -135,4 +100,14 @@ asyncio_mode = "auto" "tests/**/*.py" = [ "S101", # Tests need assertions "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + "PT011", + "PLR2004", + + # TODO + "PLC0415", + "PT012", + "D", +] +"scripts/*.py" = [ + "INP001", # Not a package ] diff --git a/libs/partners/qdrant/scripts/check_imports.py b/libs/partners/qdrant/scripts/check_imports.py index ec3fc6e95f5..71468fbacc1 100644 --- a/libs/partners/qdrant/scripts/check_imports.py +++ b/libs/partners/qdrant/scripts/check_imports.py @@ -8,7 +8,7 @@ if __name__ == "__main__": for file in files: try: SourceFileLoader("x", file).load_module() - except Exception: + except Exception: # noqa: BLE001 has_failure = True traceback.print_exc() diff --git a/libs/partners/qdrant/tests/integration_tests/async_api/test_from_texts.py b/libs/partners/qdrant/tests/integration_tests/async_api/test_from_texts.py index bd3289a0ebf..e338a835a37 100644 --- a/libs/partners/qdrant/tests/integration_tests/async_api/test_from_texts.py +++ b/libs/partners/qdrant/tests/integration_tests/async_api/test_from_texts.py @@ -124,7 +124,7 @@ async def test_qdrant_from_texts_raises_error_on_different_dimensionality( ) -> None: """Test if Qdrant.afrom_texts raises an exception if dimensionality does not match. - """ # noqa: D205 + """ collection_name = uuid.uuid4().hex await Qdrant.afrom_texts( @@ -147,7 +147,7 @@ async def test_qdrant_from_texts_raises_error_on_different_dimensionality( @pytest.mark.parametrize("location", qdrant_locations(use_in_memory=False)) @pytest.mark.parametrize( - ["first_vector_name", "second_vector_name"], + ("first_vector_name", "second_vector_name"), [ (None, "custom-vector"), ("custom-vector", None), diff --git a/libs/partners/qdrant/tests/integration_tests/common.py b/libs/partners/qdrant/tests/integration_tests/common.py index 7e8ae21a477..d84a64b64a5 100644 --- a/libs/partners/qdrant/tests/integration_tests/common.py +++ b/libs/partners/qdrant/tests/integration_tests/common.py @@ -15,7 +15,7 @@ def qdrant_running_locally() -> bool: return False -def assert_documents_equals(actual: list[Document], expected: list[Document]): # type: ignore[no-untyped-def] +def assert_documents_equals(actual: list[Document], expected: list[Document]) -> None: # type: ignore[no-untyped-def] assert len(actual) == len(expected) for actual_doc, expected_doc in zip(actual, expected): @@ -33,7 +33,7 @@ def assert_documents_equals(actual: list[Document], expected: list[Document]): class ConsistentFakeEmbeddings(Embeddings): """Fake embeddings which remember all the texts seen so far to return consistent vectors for the same texts. - """ # noqa: D205 + """ def __init__(self, dimensionality: int = 10) -> None: self.known_texts: list[str] = [] @@ -54,18 +54,18 @@ class ConsistentFakeEmbeddings(Embeddings): def embed_query(self, text: str) -> list[float]: """Return consistent embeddings for the text, if seen before, or a constant one if the text is unknown. - """ # noqa: D205 + """ return self.embed_documents([text])[0] class ConsistentFakeSparseEmbeddings(SparseEmbeddings): """Fake sparse embeddings which remembers all the texts seen so far "to return consistent vectors for the same texts. - """ # noqa: D205 + """ def __init__(self, dimensionality: int = 25) -> None: self.known_texts: list[str] = [] - self.dimensionality = 25 + self.dimensionality = dimensionality def embed_documents(self, texts: list[str]) -> list[SparseVector]: """Return consistent embeddings for each text seen so far.""" @@ -82,5 +82,5 @@ class ConsistentFakeSparseEmbeddings(SparseEmbeddings): def embed_query(self, text: str) -> SparseVector: """Return consistent embeddings for the text, if seen before, or a constant one if the text is unknown. - """ # noqa: D205 + """ return self.embed_documents([text])[0] diff --git a/libs/partners/qdrant/tests/integration_tests/fastembed/__init__.py b/libs/partners/qdrant/tests/integration_tests/fastembed/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/partners/qdrant/tests/integration_tests/fixtures.py b/libs/partners/qdrant/tests/integration_tests/fixtures.py index 0cc4d267ca6..54771cfd5b3 100644 --- a/libs/partners/qdrant/tests/integration_tests/fixtures.py +++ b/libs/partners/qdrant/tests/integration_tests/fixtures.py @@ -19,7 +19,7 @@ def qdrant_locations(use_in_memory: bool = True) -> list[str]: # noqa: FBT001, locations.append("http://localhost:6333") if qdrant_url := os.getenv("QDRANT_URL"): - logger.info(f"Running Qdrant tests with Qdrant instance at {qdrant_url}.") + logger.info("Running Qdrant tests with Qdrant instance at %s.", qdrant_url) locations.append(qdrant_url) return locations diff --git a/libs/partners/qdrant/tests/integration_tests/qdrant_vector_store/__init__.py b/libs/partners/qdrant/tests/integration_tests/qdrant_vector_store/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/libs/partners/qdrant/tests/integration_tests/qdrant_vector_store/test_from_texts.py b/libs/partners/qdrant/tests/integration_tests/qdrant_vector_store/test_from_texts.py index cfb98e823d9..2a11f7c0618 100644 --- a/libs/partners/qdrant/tests/integration_tests/qdrant_vector_store/test_from_texts.py +++ b/libs/partners/qdrant/tests/integration_tests/qdrant_vector_store/test_from_texts.py @@ -191,7 +191,7 @@ def test_qdrant_from_texts_raises_error_on_different_dimensionality( @pytest.mark.parametrize("location", qdrant_locations(use_in_memory=False)) @pytest.mark.parametrize( - ["first_vector_name", "second_vector_name"], + ("first_vector_name", "second_vector_name"), [ ("", "custom-vector"), ("custom-vector", ""), diff --git a/libs/partners/qdrant/tests/integration_tests/test_embedding_interface.py b/libs/partners/qdrant/tests/integration_tests/test_embedding_interface.py index 487cca3aacf..3e6ee388fdb 100644 --- a/libs/partners/qdrant/tests/integration_tests/test_embedding_interface.py +++ b/libs/partners/qdrant/tests/integration_tests/test_embedding_interface.py @@ -1,17 +1,19 @@ from __future__ import annotations import uuid -from typing import Callable, Optional +from typing import TYPE_CHECKING, Callable, Optional import pytest # type: ignore[import-not-found] -from langchain_core.embeddings import Embeddings from langchain_qdrant import Qdrant from tests.integration_tests.common import ConsistentFakeEmbeddings +if TYPE_CHECKING: + from langchain_core.embeddings import Embeddings + @pytest.mark.parametrize( - ["embeddings", "embedding_function"], + ("embeddings", "embedding_function"), [ (ConsistentFakeEmbeddings(), None), (ConsistentFakeEmbeddings().embed_query, None), @@ -36,7 +38,7 @@ def test_qdrant_embedding_interface( @pytest.mark.parametrize( - ["embeddings", "embedding_function"], + ("embeddings", "embedding_function"), [ (ConsistentFakeEmbeddings(), ConsistentFakeEmbeddings().embed_query), (None, None), diff --git a/libs/partners/qdrant/tests/integration_tests/test_from_texts.py b/libs/partners/qdrant/tests/integration_tests/test_from_texts.py index b2eaa8ee6bd..1b61029aedc 100644 --- a/libs/partners/qdrant/tests/integration_tests/test_from_texts.py +++ b/libs/partners/qdrant/tests/integration_tests/test_from_texts.py @@ -147,7 +147,7 @@ def test_qdrant_from_texts_raises_error_on_different_dimensionality( @pytest.mark.parametrize( - ["first_vector_name", "second_vector_name"], + ("first_vector_name", "second_vector_name"), [ (None, "custom-vector"), ("custom-vector", None), diff --git a/libs/partners/qdrant/tests/integration_tests/test_max_marginal_relevance.py b/libs/partners/qdrant/tests/integration_tests/test_max_marginal_relevance.py index 252dc621ad5..5c76f915095 100644 --- a/libs/partners/qdrant/tests/integration_tests/test_max_marginal_relevance.py +++ b/libs/partners/qdrant/tests/integration_tests/test_max_marginal_relevance.py @@ -4,6 +4,7 @@ from typing import Optional import pytest # type: ignore[import-not-found] from langchain_core.documents import Document +from qdrant_client import models from langchain_qdrant import Qdrant from tests.integration_tests.common import ( @@ -23,8 +24,6 @@ def test_qdrant_max_marginal_relevance_search( vector_name: Optional[str], ) -> None: """Test end to end construction and MRR search.""" - from qdrant_client import models - filter_ = models.Filter( must=[ models.FieldCondition( diff --git a/libs/partners/qdrant/tests/integration_tests/test_similarity_search.py b/libs/partners/qdrant/tests/integration_tests/test_similarity_search.py index 91ec974dfe6..8471cafd0b9 100644 --- a/libs/partners/qdrant/tests/integration_tests/test_similarity_search.py +++ b/libs/partners/qdrant/tests/integration_tests/test_similarity_search.py @@ -5,6 +5,7 @@ from typing import Optional import numpy as np import pytest # type: ignore[import-not-found] from langchain_core.documents import Document +from qdrant_client.http import models as rest from langchain_qdrant import Qdrant from tests.integration_tests.common import ( @@ -215,8 +216,6 @@ def test_qdrant_similarity_search_filters_with_qdrant_filters( vector_name: Optional[str], ) -> None: """Test end to end construction and search.""" - from qdrant_client.http import models as rest - texts = ["foo", "bar", "baz"] metadatas = [ {"page": i, "details": {"page": i + 1, "pages": [i + 2, -1]}} diff --git a/libs/partners/qdrant/tests/unit_tests/test_standard.py b/libs/partners/qdrant/tests/unit_tests/test_standard.py index b76ef73892c..7198cbae927 100644 --- a/libs/partners/qdrant/tests/unit_tests/test_standard.py +++ b/libs/partners/qdrant/tests/unit_tests/test_standard.py @@ -12,7 +12,7 @@ class MockEmbeddings(Embeddings): """Mock embed_documents method.""" return [[1.0, 2.0, 3.0] for _ in texts] - def embed_query(self, text: str) -> list[float]: + def embed_query(self) -> list[float]: # type: ignore[override] """Mock embed_query method.""" return [1.0, 2.0, 3.0] diff --git a/libs/partners/xai/langchain_xai/__init__.py b/libs/partners/xai/langchain_xai/__init__.py index 185dca1f042..3ba202d04d8 100644 --- a/libs/partners/xai/langchain_xai/__init__.py +++ b/libs/partners/xai/langchain_xai/__init__.py @@ -1,3 +1,5 @@ +"""LangChain integration with xAI.""" + from langchain_xai.chat_models import ChatXAI __all__ = ["ChatXAI"] diff --git a/libs/partners/xai/langchain_xai/chat_models.py b/libs/partners/xai/langchain_xai/chat_models.py index 1e67c2cf4bc..299289cbf5e 100644 --- a/libs/partners/xai/langchain_xai/chat_models.py +++ b/libs/partners/xai/langchain_xai/chat_models.py @@ -456,7 +456,9 @@ class ChatXAI(BaseChatOpenAI): # type: ignore[override] return "xai-chat" def _get_ls_params( - self, stop: Optional[list[str]] = None, **kwargs: Any + self, + stop: Optional[list[str]] = None, + **kwargs: Any, # noqa: ANN401 ) -> LangSmithParams: """Get the parameters used to invoke the model.""" params = super()._get_ls_params(stop=stop, **kwargs) @@ -581,7 +583,7 @@ class ChatXAI(BaseChatOpenAI): # type: ignore[override] ] = "function_calling", include_raw: bool = False, strict: Optional[bool] = None, - **kwargs: Any, + **kwargs: Any, # noqa: ANN401 ) -> Runnable[LanguageModelInput, _DictOrPydantic]: """Model wrapper that returns outputs formatted to match the given schema. diff --git a/libs/partners/xai/pyproject.toml b/libs/partners/xai/pyproject.toml index a32e5b8be8c..1fada049e1c 100644 --- a/libs/partners/xai/pyproject.toml +++ b/libs/partners/xai/pyproject.toml @@ -57,53 +57,8 @@ docstring-code-format = true docstring-code-line-length = 100 [tool.ruff.lint] -select = [ - "A", # flake8-builtins - "B", # flake8-bugbear - "ASYNC", # flake8-async - "C4", # flake8-comprehensions - "COM", # flake8-commas - "D", # pydocstyle - "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 -] +select = ["ALL"] ignore = [ - "D100", # pydocstyle: Missing docstring in public module - "D101", # pydocstyle: Missing docstring in public class - "D102", # pydocstyle: Missing docstring in public method - "D103", # pydocstyle: Missing docstring in public function - "D104", # pydocstyle: Missing docstring in public package - "D105", # pydocstyle: Missing docstring in magic method - "D107", # pydocstyle: Missing docstring in __init__ - "D203", # Messes with the formatter - "D213", # pydocstyle: Multi-line docstring summary should start at the second line (incompatible with D212) - "D407", # pydocstyle: Missing-dashed-underline-after-section "COM812", # Messes with the formatter "ISC001", # Messes with the formatter "PERF203", # Rarely useful @@ -112,11 +67,14 @@ ignore = [ "SLF001", # Private member access "UP007", # pyupgrade: non-pep604-annotation-union "UP045", # pyupgrade: non-pep604-annotation-optional + "FIX", # TODOs + "TD", # TODOs ] unfixable = ["B028"] # People should intentionally tune the stacklevel [tool.ruff.lint.pydocstyle] convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters [tool.ruff.lint.per-file-ignores] "tests/**" = ["D"] @@ -125,6 +83,13 @@ convention = "google" "tests/**/*.py" = [ "S101", # Tests need assertions "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + + # TODO + "PT011", + "PLR2004", +] +"scripts/*.py" = [ + "INP001", # Not a package ] [tool.coverage.run] diff --git a/libs/partners/xai/scripts/check_imports.py b/libs/partners/xai/scripts/check_imports.py index d9ab373d35e..e0dc0dbab4c 100644 --- a/libs/partners/xai/scripts/check_imports.py +++ b/libs/partners/xai/scripts/check_imports.py @@ -10,7 +10,7 @@ if __name__ == "__main__": for file in files: try: SourceFileLoader("x", file).load_module() - except Exception: + except Exception: # noqa: BLE001 has_failure = True traceback.print_exc() diff --git a/libs/standard-tests/pyproject.toml b/libs/standard-tests/pyproject.toml index a00132f177f..2c6c1deaf0d 100644 --- a/libs/standard-tests/pyproject.toml +++ b/libs/standard-tests/pyproject.toml @@ -94,9 +94,12 @@ flake8-annotations.allow-star-arg-any = true flake8-annotations.mypy-init-return = true flake8-type-checking.runtime-evaluated-base-classes = ["pydantic.BaseModel","langchain_core.load.serializable.Serializable","langchain_core.runnables.base.RunnableSerializable"] pep8-naming.classmethod-decorators = [ "classmethod", "langchain_core.utils.pydantic.pre_init", "pydantic.field_validator", "pydantic.v1.root_validator",] -pydocstyle.convention = "google" pyupgrade.keep-runtime-typing = true +[tool.ruff.lint.pydocstyle] +convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters + [tool.ruff.lint.per-file-ignores] "tests/**" = [ "D1",] "scripts/**" = [ "INP",] diff --git a/libs/text-splitters/pyproject.toml b/libs/text-splitters/pyproject.toml index af14584a3dc..6239ff909c5 100644 --- a/libs/text-splitters/pyproject.toml +++ b/libs/text-splitters/pyproject.toml @@ -96,9 +96,13 @@ flake8-annotations.allow-star-arg-any = true flake8-annotations.mypy-init-return = true flake8-type-checking.runtime-evaluated-base-classes = ["pydantic.BaseModel","langchain_core.load.serializable.Serializable","langchain_core.runnables.base.RunnableSerializable"] pep8-naming.classmethod-decorators = [ "classmethod", "langchain_core.utils.pydantic.pre_init", "pydantic.field_validator", "pydantic.v1.root_validator",] -pydocstyle.convention = "google" pyupgrade.keep-runtime-typing = true +[tool.ruff.lint.pydocstyle] +convention = "google" +ignore-var-parameters = true # ignore missing documentation for *args and **kwargs parameters + + [tool.ruff.lint.per-file-ignores] "scripts/**" = [ "D1", # Docstrings not mandatory in scripts