ollama[patch]: ruff fixes and rules (#31924)

* bump ruff deps
* add more thorough ruff rules
* fix said rules
This commit is contained in:
Mason Daugherty 2025-07-08 13:42:19 -04:00 committed by GitHub
parent 4d9eefecab
commit 1f829aacf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 179 additions and 108 deletions

View File

@ -41,13 +41,14 @@ lint_tests: PYTHON_FILES=tests
lint_tests: MYPY_CACHE=.mypy_cache_test lint_tests: MYPY_CACHE=.mypy_cache_test
lint lint_diff lint_package lint_tests: lint lint_diff lint_package lint_tests:
[ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff check $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff format $(PYTHON_FILES) --diff [ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff format $(PYTHON_FILES) --diff
[ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) && uv run --all-groups mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE) [ "$(PYTHON_FILES)" = "" ] || mkdir -p $(MYPY_CACHE) && uv run --all-groups mypy $(PYTHON_FILES) --cache-dir $(MYPY_CACHE)
format format_diff: format format_diff:
[ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff format $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff format $(PYTHON_FILES)
[ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff --fix $(PYTHON_FILES) [ "$(PYTHON_FILES)" = "" ] || uv run --all-groups ruff check --fix $(PYTHON_FILES)
spell_check: spell_check:
uv run --all-groups codespell --toml pyproject.toml uv run --all-groups codespell --toml pyproject.toml

View File

@ -18,7 +18,7 @@ del metadata # optional, avoids polluting the results of dir(__package__)
__all__ = [ __all__ = [
"ChatOllama", "ChatOllama",
"OllamaLLM",
"OllamaEmbeddings", "OllamaEmbeddings",
"OllamaLLM",
"__version__", "__version__",
] ]

View File

@ -20,18 +20,21 @@ def validate_model(client: Client, model_name: str) -> None:
if not any( if not any(
model_name == m or m.startswith(f"{model_name}:") for m in model_names model_name == m or m.startswith(f"{model_name}:") for m in model_names
): ):
raise ValueError( msg = (
f"Model `{model_name}` not found in Ollama. Please pull the " f"Model `{model_name}` not found in Ollama. Please pull the "
f"model (using `ollama pull {model_name}`) or specify a valid " f"model (using `ollama pull {model_name}`) or specify a valid "
f"model name. Available local models: {', '.join(model_names)}" f"model name. Available local models: {', '.join(model_names)}"
) )
raise ValueError(msg)
except ConnectError as e: except ConnectError as e:
raise ValueError( msg = (
"Connection to Ollama failed. Please make sure Ollama is running " "Connection to Ollama failed. Please make sure Ollama is running "
f"and accessible at {client._client.base_url}. " f"and accessible at {client._client.base_url}. "
) from e )
raise ValueError(msg) from e
except ResponseError as e: except ResponseError as e:
raise ValueError( msg = (
"Received an error from the Ollama API. " "Received an error from the Ollama API. "
"Please check your Ollama server logs." "Please check your Ollama server logs."
) from e )
raise ValueError(msg) from e

View File

@ -1,5 +1,7 @@
"""Ollama chat models.""" """Ollama chat models."""
from __future__ import annotations
import json import json
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
from operator import itemgetter from operator import itemgetter
@ -74,7 +76,9 @@ def _get_usage_metadata_from_generation_info(
def _parse_json_string( def _parse_json_string(
json_string: str, raw_tool_call: dict[str, Any], skip: bool json_string: str,
raw_tool_call: dict[str, Any],
skip: bool, # noqa: FBT001
) -> Any: ) -> Any:
"""Attempt to parse a JSON string for tool calling. """Attempt to parse a JSON string for tool calling.
@ -148,16 +152,19 @@ def _get_tool_calls_from_response(
) -> list[ToolCall]: ) -> list[ToolCall]:
"""Get tool calls from ollama response.""" """Get tool calls from ollama response."""
tool_calls = [] tool_calls = []
if "message" in response: if "message" in response and (
if raw_tool_calls := response["message"].get("tool_calls"): raw_tool_calls := response["message"].get("tool_calls")
for tc in raw_tool_calls: ):
tool_calls.append( tool_calls.extend(
tool_call( [
id=str(uuid4()), tool_call(
name=tc["function"]["name"], id=str(uuid4()),
args=_parse_arguments_from_tool_call(tc) or {}, name=tc["function"]["name"],
) args=_parse_arguments_from_tool_call(tc) or {},
) )
for tc in raw_tool_calls
]
)
return tool_calls return tool_calls
@ -178,14 +185,12 @@ def _get_image_from_data_content_block(block: dict) -> str:
if block["type"] == "image": if block["type"] == "image":
if block["source_type"] == "base64": if block["source_type"] == "base64":
return block["data"] return block["data"]
else: error_message = "Image data only supported through in-line base64 format."
error_message = "Image data only supported through in-line base64 format."
raise ValueError(error_message)
else:
error_message = f"Blocks of type {block['type']} not supported."
raise ValueError(error_message) raise ValueError(error_message)
error_message = f"Blocks of type {block['type']} not supported."
raise ValueError(error_message)
def _is_pydantic_class(obj: Any) -> bool: def _is_pydantic_class(obj: Any) -> bool:
return isinstance(obj, type) and is_basemodel_subclass(obj) return isinstance(obj, type) and is_basemodel_subclass(obj)
@ -459,7 +464,7 @@ class ChatOllama(BaseChatModel):
"""Base url the model is hosted under.""" """Base url the model is hosted under."""
client_kwargs: Optional[dict] = {} client_kwargs: Optional[dict] = {}
"""Additional kwargs to pass to the httpx clients. """Additional kwargs to pass to the httpx clients.
These arguments are passed to both synchronous and async clients. These arguments are passed to both synchronous and async clients.
Use sync_client_kwargs and async_client_kwargs to pass different arguments Use sync_client_kwargs and async_client_kwargs to pass different arguments
to synchronous and asynchronous clients. to synchronous and asynchronous clients.
@ -496,7 +501,8 @@ class ChatOllama(BaseChatModel):
ollama_messages = self._convert_messages_to_ollama_messages(messages) ollama_messages = self._convert_messages_to_ollama_messages(messages)
if self.stop is not None and stop is not None: if self.stop is not None and stop is not None:
raise ValueError("`stop` found in both the input and default params.") msg = "`stop` found in both the input and default params."
raise ValueError(msg)
if self.stop is not None: if self.stop is not None:
stop = self.stop stop = self.stop
@ -584,7 +590,8 @@ class ChatOllama(BaseChatModel):
role = "tool" role = "tool"
tool_call_id = message.tool_call_id tool_call_id = message.tool_call_id
else: else:
raise ValueError("Received unsupported message type for Ollama.") msg = "Received unsupported message type for Ollama."
raise ValueError(msg)
content = "" content = ""
images = [] images = []
@ -608,10 +615,11 @@ class ChatOllama(BaseChatModel):
): ):
image_url = temp_image_url["url"] image_url = temp_image_url["url"]
else: else:
raise ValueError( msg = (
"Only string image_url or dict with string 'url' " "Only string image_url or dict with string 'url' "
"inside content parts are supported." "inside content parts are supported."
) )
raise ValueError(msg)
image_url_components = image_url.split(",") image_url_components = image_url.split(",")
# Support data:image/jpeg;base64,<image> format # Support data:image/jpeg;base64,<image> format
@ -624,22 +632,24 @@ class ChatOllama(BaseChatModel):
image = _get_image_from_data_content_block(content_part) image = _get_image_from_data_content_block(content_part)
images.append(image) images.append(image)
else: else:
raise ValueError( msg = (
"Unsupported message content type. " "Unsupported message content type. "
"Must either have type 'text' or type 'image_url' " "Must either have type 'text' or type 'image_url' "
"with a string 'image_url' field." "with a string 'image_url' field."
) )
# Should convert to ollama.Message once role includes tool, and tool_call_id is in Message # noqa: E501 raise ValueError(msg)
msg: dict = { # Should convert to ollama.Message once role includes tool,
# and tool_call_id is in Message
msg_: dict = {
"role": role, "role": role,
"content": content, "content": content,
"images": images, "images": images,
} }
if tool_calls: if tool_calls:
msg["tool_calls"] = tool_calls msg_["tool_calls"] = tool_calls
if tool_call_id: if tool_call_id:
msg["tool_call_id"] = tool_call_id msg_["tool_call_id"] = tool_call_id
ollama_messages.append(msg) ollama_messages.append(msg_)
return ollama_messages return ollama_messages
@ -677,7 +687,7 @@ class ChatOllama(BaseChatModel):
messages: list[BaseMessage], messages: list[BaseMessage],
stop: Optional[list[str]] = None, stop: Optional[list[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None, run_manager: Optional[CallbackManagerForLLMRun] = None,
verbose: bool = False, verbose: bool = False, # noqa: FBT001, FBT002
**kwargs: Any, **kwargs: Any,
) -> ChatGenerationChunk: ) -> ChatGenerationChunk:
final_chunk = None final_chunk = None
@ -693,7 +703,8 @@ class ChatOllama(BaseChatModel):
verbose=verbose, verbose=verbose,
) )
if final_chunk is None: if final_chunk is None:
raise ValueError("No data received from Ollama stream.") msg = "No data received from Ollama stream."
raise ValueError(msg)
return final_chunk return final_chunk
@ -702,7 +713,7 @@ class ChatOllama(BaseChatModel):
messages: list[BaseMessage], messages: list[BaseMessage],
stop: Optional[list[str]] = None, stop: Optional[list[str]] = None,
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
verbose: bool = False, verbose: bool = False, # noqa: FBT001, FBT002
**kwargs: Any, **kwargs: Any,
) -> ChatGenerationChunk: ) -> ChatGenerationChunk:
final_chunk = None final_chunk = None
@ -718,7 +729,8 @@ class ChatOllama(BaseChatModel):
verbose=verbose, verbose=verbose,
) )
if final_chunk is None: if final_chunk is None:
raise ValueError("No data received from Ollama stream.") msg = "No data received from Ollama stream."
raise ValueError(msg)
return final_chunk return final_chunk
@ -908,7 +920,7 @@ class ChatOllama(BaseChatModel):
self, self,
tools: Sequence[Union[dict[str, Any], type, Callable, BaseTool]], tools: Sequence[Union[dict[str, Any], type, Callable, BaseTool]],
*, *,
tool_choice: Optional[Union[dict, str, Literal["auto", "any"], bool]] = None, tool_choice: Optional[Union[dict, str, Literal["auto", "any"], bool]] = None, # noqa: PYI051
**kwargs: Any, **kwargs: Any,
) -> Runnable[LanguageModelInput, BaseMessage]: ) -> Runnable[LanguageModelInput, BaseMessage]:
"""Bind tool-like objects to this chat model. """Bind tool-like objects to this chat model.
@ -923,7 +935,7 @@ class ChatOllama(BaseChatModel):
is currently ignored as it is not supported by Ollama.** is currently ignored as it is not supported by Ollama.**
kwargs: Any additional parameters are passed directly to kwargs: Any additional parameters are passed directly to
``self.bind(**kwargs)``. ``self.bind(**kwargs)``.
""" # noqa: E501 """
formatted_tools = [convert_to_openai_tool(tool) for tool in tools] formatted_tools = [convert_to_openai_tool(tool) for tool in tools]
return super().bind(tools=formatted_tools, **kwargs) return super().bind(tools=formatted_tools, **kwargs)
@ -1180,14 +1192,16 @@ class ChatOllama(BaseChatModel):
""" # noqa: E501, D301 """ # noqa: E501, D301
_ = kwargs.pop("strict", None) _ = kwargs.pop("strict", None)
if kwargs: if kwargs:
raise ValueError(f"Received unsupported arguments {kwargs}") msg = f"Received unsupported arguments {kwargs}"
raise ValueError(msg)
is_pydantic_schema = _is_pydantic_class(schema) is_pydantic_schema = _is_pydantic_class(schema)
if method == "function_calling": if method == "function_calling":
if schema is None: if schema is None:
raise ValueError( msg = (
"schema must be specified when method is not 'json_mode'. " "schema must be specified when method is not 'json_mode'. "
"Received None." "Received None."
) )
raise ValueError(msg)
formatted_tool = convert_to_openai_tool(schema) formatted_tool = convert_to_openai_tool(schema)
tool_name = formatted_tool["function"]["name"] tool_name = formatted_tool["function"]["name"]
llm = self.bind_tools( llm = self.bind_tools(
@ -1222,10 +1236,11 @@ class ChatOllama(BaseChatModel):
) )
elif method == "json_schema": elif method == "json_schema":
if schema is None: if schema is None:
raise ValueError( msg = (
"schema must be specified when method is not 'json_mode'. " "schema must be specified when method is not 'json_mode'. "
"Received None." "Received None."
) )
raise ValueError(msg)
if is_pydantic_schema: if is_pydantic_schema:
schema = cast(TypeBaseModel, schema) schema = cast(TypeBaseModel, schema)
if issubclass(schema, BaseModelV1): if issubclass(schema, BaseModelV1):
@ -1259,10 +1274,11 @@ class ChatOllama(BaseChatModel):
) )
output_parser = JsonOutputParser() output_parser = JsonOutputParser()
else: else:
raise ValueError( msg = (
f"Unrecognized method argument. Expected one of 'function_calling', " f"Unrecognized method argument. Expected one of 'function_calling', "
f"'json_schema', or 'json_mode'. Received: '{method}'" f"'json_schema', or 'json_mode'. Received: '{method}'"
) )
raise ValueError(msg)
if include_raw: if include_raw:
parser_assign = RunnablePassthrough.assign( parser_assign = RunnablePassthrough.assign(

View File

@ -1,5 +1,7 @@
"""Ollama embeddings models.""" """Ollama embeddings models."""
from __future__ import annotations
from typing import Any, Optional from typing import Any, Optional
from langchain_core.embeddings import Embeddings from langchain_core.embeddings import Embeddings
@ -132,7 +134,7 @@ class OllamaEmbeddings(BaseModel, Embeddings):
"""Base url the model is hosted under.""" """Base url the model is hosted under."""
client_kwargs: Optional[dict] = {} client_kwargs: Optional[dict] = {}
"""Additional kwargs to pass to the httpx clients. """Additional kwargs to pass to the httpx clients.
These arguments are passed to both synchronous and async clients. These arguments are passed to both synchronous and async clients.
Use sync_client_kwargs and async_client_kwargs to pass different arguments Use sync_client_kwargs and async_client_kwargs to pass different arguments
to synchronous and asynchronous clients. to synchronous and asynchronous clients.
@ -271,14 +273,14 @@ class OllamaEmbeddings(BaseModel, Embeddings):
def embed_documents(self, texts: list[str]) -> list[list[float]]: def embed_documents(self, texts: list[str]) -> list[list[float]]:
"""Embed search docs.""" """Embed search docs."""
if not self._client: if not self._client:
raise ValueError( msg = (
"Ollama client is not initialized. " "Ollama client is not initialized. "
"Please ensure Ollama is running and the model is loaded." "Please ensure Ollama is running and the model is loaded."
) )
embedded_docs = self._client.embed( raise ValueError(msg)
return self._client.embed(
self.model, texts, options=self._default_params, keep_alive=self.keep_alive self.model, texts, options=self._default_params, keep_alive=self.keep_alive
)["embeddings"] )["embeddings"]
return embedded_docs
def embed_query(self, text: str) -> list[float]: def embed_query(self, text: str) -> list[float]:
"""Embed query text.""" """Embed query text."""
@ -287,16 +289,16 @@ class OllamaEmbeddings(BaseModel, Embeddings):
async def aembed_documents(self, texts: list[str]) -> list[list[float]]: async def aembed_documents(self, texts: list[str]) -> list[list[float]]:
"""Embed search docs.""" """Embed search docs."""
if not self._async_client: if not self._async_client:
raise ValueError( msg = (
"Ollama client is not initialized. " "Ollama client is not initialized. "
"Please ensure Ollama is running and the model is loaded." "Please ensure Ollama is running and the model is loaded."
) )
embedded_docs = ( raise ValueError(msg)
return (
await self._async_client.embed( await self._async_client.embed(
self.model, texts, keep_alive=self.keep_alive self.model, texts, keep_alive=self.keep_alive
) )
)["embeddings"] )["embeddings"]
return embedded_docs
async def aembed_query(self, text: str) -> list[float]: async def aembed_query(self, text: str) -> list[float]:
"""Embed query text.""" """Embed query text."""

View File

@ -1,5 +1,7 @@
"""Ollama large language models.""" """Ollama large language models."""
from __future__ import annotations
from collections.abc import AsyncIterator, Iterator, Mapping from collections.abc import AsyncIterator, Iterator, Mapping
from typing import ( from typing import (
Any, Any,
@ -132,22 +134,22 @@ class OllamaLLM(BaseLLM):
"""Base url the model is hosted under.""" """Base url the model is hosted under."""
client_kwargs: Optional[dict] = {} client_kwargs: Optional[dict] = {}
"""Additional kwargs to pass to the httpx clients. """Additional kwargs to pass to the httpx clients.
These arguments are passed to both synchronous and async clients. These arguments are passed to both synchronous and async clients.
Use sync_client_kwargs and async_client_kwargs to pass different arguments Use sync_client_kwargs and async_client_kwargs to pass different arguments
to synchronous and asynchronous clients. to synchronous and asynchronous clients.
""" """
async_client_kwargs: Optional[dict] = {} async_client_kwargs: Optional[dict] = {}
"""Additional kwargs to merge with client_kwargs before passing to the HTTPX """Additional kwargs to merge with client_kwargs before passing to the HTTPX
AsyncClient. AsyncClient.
For a full list of the params, see the `HTTPX documentation <https://www.python-httpx.org/api/#asyncclient>`__. For a full list of the params, see the `HTTPX documentation <https://www.python-httpx.org/api/#asyncclient>`__.
""" """
sync_client_kwargs: Optional[dict] = {} sync_client_kwargs: Optional[dict] = {}
"""Additional kwargs to merge with client_kwargs before passing to the HTTPX Client. """Additional kwargs to merge with client_kwargs before passing to the HTTPX Client.
For a full list of the params, see the `HTTPX documentation <https://www.python-httpx.org/api/#client>`__. For a full list of the params, see the `HTTPX documentation <https://www.python-httpx.org/api/#client>`__.
""" """
@ -168,7 +170,8 @@ class OllamaLLM(BaseLLM):
**kwargs: Any, **kwargs: Any,
) -> dict[str, Any]: ) -> dict[str, Any]:
if self.stop is not None and stop is not None: if self.stop is not None and stop is not None:
raise ValueError("`stop` found in both the input and default params.") msg = "`stop` found in both the input and default params."
raise ValueError(msg)
if self.stop is not None: if self.stop is not None:
stop = self.stop stop = self.stop
@ -193,7 +196,7 @@ class OllamaLLM(BaseLLM):
}, },
) )
params = { return {
"prompt": prompt, "prompt": prompt,
"stream": kwargs.pop("stream", True), "stream": kwargs.pop("stream", True),
"model": kwargs.pop("model", self.model), "model": kwargs.pop("model", self.model),
@ -204,8 +207,6 @@ class OllamaLLM(BaseLLM):
**kwargs, **kwargs,
} }
return params
@property @property
def _llm_type(self) -> str: def _llm_type(self) -> str:
"""Return type of LLM.""" """Return type of LLM."""
@ -267,14 +268,14 @@ class OllamaLLM(BaseLLM):
prompt: str, prompt: str,
stop: Optional[list[str]] = None, stop: Optional[list[str]] = None,
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
verbose: bool = False, verbose: bool = False, # noqa: FBT001, FBT002
**kwargs: Any, **kwargs: Any,
) -> GenerationChunk: ) -> GenerationChunk:
final_chunk = None final_chunk = None
async for stream_resp in self._acreate_generate_stream(prompt, stop, **kwargs): async for stream_resp in self._acreate_generate_stream(prompt, stop, **kwargs):
if not isinstance(stream_resp, str): if not isinstance(stream_resp, str):
chunk = GenerationChunk( chunk = GenerationChunk(
text=stream_resp["response"] if "response" in stream_resp else "", text=stream_resp.get("response", ""),
generation_info=( generation_info=(
dict(stream_resp) if stream_resp.get("done") is True else None dict(stream_resp) if stream_resp.get("done") is True else None
), ),
@ -290,7 +291,8 @@ class OllamaLLM(BaseLLM):
verbose=verbose, verbose=verbose,
) )
if final_chunk is None: if final_chunk is None:
raise ValueError("No data received from Ollama stream.") msg = "No data received from Ollama stream."
raise ValueError(msg)
return final_chunk return final_chunk
@ -299,14 +301,14 @@ class OllamaLLM(BaseLLM):
prompt: str, prompt: str,
stop: Optional[list[str]] = None, stop: Optional[list[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None, run_manager: Optional[CallbackManagerForLLMRun] = None,
verbose: bool = False, verbose: bool = False, # noqa: FBT001, FBT002
**kwargs: Any, **kwargs: Any,
) -> GenerationChunk: ) -> GenerationChunk:
final_chunk = None final_chunk = None
for stream_resp in self._create_generate_stream(prompt, stop, **kwargs): for stream_resp in self._create_generate_stream(prompt, stop, **kwargs):
if not isinstance(stream_resp, str): if not isinstance(stream_resp, str):
chunk = GenerationChunk( chunk = GenerationChunk(
text=stream_resp["response"] if "response" in stream_resp else "", text=stream_resp.get("response", ""),
generation_info=( generation_info=(
dict(stream_resp) if stream_resp.get("done") is True else None dict(stream_resp) if stream_resp.get("done") is True else None
), ),
@ -322,7 +324,8 @@ class OllamaLLM(BaseLLM):
verbose=verbose, verbose=verbose,
) )
if final_chunk is None: if final_chunk is None:
raise ValueError("No data received from Ollama stream.") msg = "No data received from Ollama stream."
raise ValueError(msg)
return final_chunk return final_chunk

View File

@ -48,15 +48,62 @@ target-version = "py39"
[tool.ruff.lint] [tool.ruff.lint]
select = [ select = [
"E", # pycodestyle "A", # flake8-builtins
"F", # pyflakes "B", # flake8-bugbear
"I", # isort "ASYNC", # flake8-async
"T201", # print "C4", # flake8-comprehensions
"D", # pydocstyle "COM", # flake8-commas
"UP", # pyupgrade "D", # pydocstyle
"S", # flake8-bandit "DOC", # pydoclint
"E", # pycodestyle error
"EM", # flake8-errmsg
"F", # pyflakes
"FA", # flake8-future-annotations
"FBT", # flake8-boolean-trap
"FLY", # flake8-flynt
"I", # isort
"ICN", # flake8-import-conventions
"INT", # flake8-gettext
"ISC", # isort-comprehensions
"PGH", # pygrep-hooks
"PIE", # flake8-pie
"PERF", # flake8-perf
"PYI", # flake8-pyi
"Q", # flake8-quotes
"RET", # flake8-return
"RSE", # flake8-rst-docstrings
"RUF", # ruff
"S", # flake8-bandit
"SLF", # flake8-self
"SLOT", # flake8-slots
"SIM", # flake8-simplify
"T10", # flake8-debugger
"T20", # flake8-print
"TID", # flake8-tidy-imports
"UP", # pyupgrade
"W", # pycodestyle warning
"YTT", # flake8-2020
] ]
ignore = [ "UP007", ] 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
"S112", # Rarely useful
"RUF012", # Doesn't play well with Pydantic
"SLF001", # Private member access
"UP007", # pyupgrade: non-pep604-annotation-union
"UP045", # pyupgrade: non-pep604-annotation-optional
]
unfixable = ["B028"] # People should intentionally tune the stacklevel
[tool.ruff.lint.pydocstyle] [tool.ruff.lint.pydocstyle]
convention = "google" convention = "google"

View File

@ -1,5 +1,7 @@
"""Ollama specific chat model integration tests""" """Ollama specific chat model integration tests"""
from __future__ import annotations
from typing import Annotated, Optional from typing import Annotated, Optional
import pytest import pytest

View File

@ -21,8 +21,7 @@ def get_current_weather(location: str) -> dict:
"""Gets the current weather in a given location.""" """Gets the current weather in a given location."""
if "boston" in location.lower(): if "boston" in location.lower():
return {"temperature": "15°F", "conditions": "snow"} return {"temperature": "15°F", "conditions": "snow"}
else: return {"temperature": "unknown", "conditions": "unknown"}
return {"temperature": "unknown", "conditions": "unknown"}
class TestChatOllama(ChatModelIntegrationTests): class TestChatOllama(ChatModelIntegrationTests):
@ -65,16 +64,15 @@ class TestChatOllama(ChatModelIntegrationTests):
if chunk.tool_call_chunks: if chunk.tool_call_chunks:
tool_chunk_found = True tool_chunk_found = True
for tc_chunk in chunk.tool_call_chunks: collected_tool_chunks.extend(chunk.tool_call_chunks)
collected_tool_chunks.append(tc_chunk)
if chunk.tool_calls: if chunk.tool_calls:
final_tool_calls.extend(chunk.tool_calls) final_tool_calls.extend(chunk.tool_calls)
assert tool_chunk_found, "Tool streaming did not produce any tool_call_chunks." assert tool_chunk_found, "Tool streaming did not produce any tool_call_chunks."
assert ( assert len(final_tool_calls) == 1, (
len(final_tool_calls) == 1 f"Expected 1 final tool call, but got {len(final_tool_calls)}"
), f"Expected 1 final tool call, but got {len(final_tool_calls)}" )
final_tool_call = final_tool_calls[0] final_tool_call = final_tool_calls[0]
assert final_tool_call["name"] == "get_current_weather" assert final_tool_call["name"] == "get_current_weather"
@ -110,16 +108,15 @@ class TestChatOllama(ChatModelIntegrationTests):
if chunk.tool_call_chunks: if chunk.tool_call_chunks:
tool_chunk_found = True tool_chunk_found = True
for tc_chunk in chunk.tool_call_chunks: collected_tool_chunks.extend(chunk.tool_call_chunks)
collected_tool_chunks.append(tc_chunk)
if chunk.tool_calls: if chunk.tool_calls:
final_tool_calls.extend(chunk.tool_calls) final_tool_calls.extend(chunk.tool_calls)
assert tool_chunk_found, "Tool streaming did not produce any tool_call_chunks." assert tool_chunk_found, "Tool streaming did not produce any tool_call_chunks."
assert ( assert len(final_tool_calls) == 1, (
len(final_tool_calls) == 1 f"Expected 1 final tool call, but got {len(final_tool_calls)}"
), f"Expected 1 final tool call, but got {len(final_tool_calls)}" )
final_tool_call = final_tool_calls[0] final_tool_call = final_tool_calls[0]
assert final_tool_call["name"] == "get_current_weather" assert final_tool_call["name"] == "get_current_weather"

View File

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

View File

@ -305,7 +305,7 @@ wheels = [
[[package]] [[package]]
name = "langchain-core" name = "langchain-core"
version = "0.3.66" version = "0.3.68"
source = { editable = "../../core" } source = { editable = "../../core" }
dependencies = [ dependencies = [
{ name = "jsonpatch" }, { name = "jsonpatch" },
@ -334,7 +334,7 @@ dev = [
{ name = "jupyter", specifier = ">=1.0.0,<2.0.0" }, { name = "jupyter", specifier = ">=1.0.0,<2.0.0" },
{ name = "setuptools", specifier = ">=67.6.1,<68.0.0" }, { name = "setuptools", specifier = ">=67.6.1,<68.0.0" },
] ]
lint = [{ name = "ruff", specifier = ">=0.11.2,<0.12.0" }] lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
test = [ test = [
{ name = "blockbuster", specifier = "~=1.5.18" }, { name = "blockbuster", specifier = "~=1.5.18" },
{ name = "freezegun", specifier = ">=1.2.2,<2.0.0" }, { name = "freezegun", specifier = ">=1.2.2,<2.0.0" },
@ -403,7 +403,7 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
codespell = [{ name = "codespell", specifier = ">=2.2.6,<3.0.0" }] codespell = [{ name = "codespell", specifier = ">=2.2.6,<3.0.0" }]
dev = [{ name = "langchain-core", editable = "../../core" }] dev = [{ name = "langchain-core", editable = "../../core" }]
lint = [{ name = "ruff", specifier = ">=0.1.8,<1.0.0" }] lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
test = [ test = [
{ name = "langchain-core", editable = "../../core" }, { name = "langchain-core", editable = "../../core" },
{ name = "langchain-tests", editable = "../../standard-tests" }, { name = "langchain-tests", editable = "../../standard-tests" },
@ -456,7 +456,7 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }] codespell = [{ name = "codespell", specifier = ">=2.2.0,<3.0.0" }]
lint = [{ name = "ruff", specifier = ">=0.9.2,<1.0.0" }] lint = [{ name = "ruff", specifier = ">=0.12.2,<0.13" }]
test = [{ name = "langchain-core", editable = "../../core" }] test = [{ name = "langchain-core", editable = "../../core" }]
test-integration = [] test-integration = []
typing = [ typing = [
@ -1342,26 +1342,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.1.15" version = "0.12.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/42/33/7165f88a156be1c2fd13a18b3af6e75bbf82da5b6978cd2128d666accc18/ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e", size = 1971643, upload-time = "2024-01-29T23:06:05.541Z" } sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/11/2c/fac0658910ea3ea87a23583e58277533154261b73f9460388eb2e6e02e8f/ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df", size = 14357437, upload-time = "2024-01-29T23:05:04.991Z" }, { url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
{ url = "https://files.pythonhosted.org/packages/5b/c1/2116927385c761ffb786dfb77654a634ecd7803dee4de3b47b59536374f1/ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f", size = 7329669, upload-time = "2024-01-29T23:05:12.437Z" }, { url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
{ url = "https://files.pythonhosted.org/packages/18/d7/2199ecb42cef4d70de0e72ce4ca8878d060e25fe4434cb66f51e26158a2a/ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8", size = 7137343, upload-time = "2024-01-29T23:05:16.159Z" }, { url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
{ url = "https://files.pythonhosted.org/packages/bb/e0/8a6f9db2c5b8c7108c7e7347cd6beca805d1b2ae618569c72f2515d11e52/ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807", size = 6563223, upload-time = "2024-01-29T23:05:19.687Z" }, { url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
{ url = "https://files.pythonhosted.org/packages/98/fa/2a627747a5a5f7e1d3447704f795fd35d486460838485762cd569ef8eb0e/ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec", size = 7534853, upload-time = "2024-01-29T23:05:23.18Z" }, { url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
{ url = "https://files.pythonhosted.org/packages/55/09/c09d0f9b41d1f5e3de117579f2fcdb7063fd76cd92d6614eae1b77ccbccb/ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5", size = 8168826, upload-time = "2024-01-29T23:05:26.544Z" }, { url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
{ url = "https://files.pythonhosted.org/packages/72/48/c9dfc2c87dc6b92446d8092c2be25b42ca4fb201cecb2499996ccf483c34/ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e", size = 7942963, upload-time = "2024-01-29T23:05:30.655Z" }, { url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
{ url = "https://files.pythonhosted.org/packages/0c/57/dbc885f94450335fcff82301c4b25cf614894e79d9afbd249714e709ab42/ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b", size = 8524998, upload-time = "2024-01-29T23:05:34.503Z" }, { url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
{ url = "https://files.pythonhosted.org/packages/39/75/8dea2fc156ae525971fdada8723f78e605dcf89428f5686728438b12f9ef/ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1", size = 7534144, upload-time = "2024-01-29T23:05:38.642Z" }, { url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
{ url = "https://files.pythonhosted.org/packages/47/41/96b770475c46590bfd051ca0c5f797b2d45f2638c45f3a9daf1ae55b96d6/ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5", size = 7055002, upload-time = "2024-01-29T23:05:41.955Z" }, { url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ca/4066dbcc3631a4efe1fe695f42f20aca50474d760b3bd8e57d7565d75aa5/ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2", size = 6552130, upload-time = "2024-01-29T23:05:45.487Z" }, { url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
{ url = "https://files.pythonhosted.org/packages/b8/85/da93f0fc8f2424cf776fcce6daef9291162345179d16faf1401ff2890068/ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852", size = 7214386, upload-time = "2024-01-29T23:05:48.346Z" }, { url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
{ url = "https://files.pythonhosted.org/packages/e5/bf/de34ad339e0d1f6faa858cbcf793f3abc168b7aa516dd9227d843b992be8/ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447", size = 7602787, upload-time = "2024-01-29T23:05:51.341Z" }, { url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
{ url = "https://files.pythonhosted.org/packages/8d/61/ffdccecb0b39521d7060d6a6bc33c53d7f20d48d3511d6333cb01f26e979/ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f", size = 6670488, upload-time = "2024-01-29T23:05:54.454Z" }, { url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
{ url = "https://files.pythonhosted.org/packages/2b/5f/3ba51cc770ed2b2df88efc32bba26759e6ac5c6149319a60913a85230936/ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587", size = 7319395, upload-time = "2024-01-29T23:05:58.135Z" }, { url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
{ url = "https://files.pythonhosted.org/packages/c9/bd/c196493563d6bf8fe960f10b83926a3fae3a43a96eac6b263aecb96c61d7/ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360", size = 6998592, upload-time = "2024-01-29T23:06:01.904Z" }, { url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
] ]
[[package]] [[package]]