mirror of
https://github.com/hwchase17/langchain.git
synced 2026-04-21 19:27:58 +00:00
Compare commits
10 Commits
jacob/meta
...
sr/deferre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0545feead7 | ||
|
|
e872675c28 | ||
|
|
3a36d4cd2f | ||
|
|
0f042990e3 | ||
|
|
264a82c6ef | ||
|
|
ce04a922e3 | ||
|
|
50b7456d80 | ||
|
|
9ab81a0908 | ||
|
|
7b7aced419 | ||
|
|
8fec4e7cee |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -166,3 +166,4 @@ virtualenv/
|
||||
scratch/
|
||||
|
||||
.langgraph_api/
|
||||
.worktrees/
|
||||
|
||||
@@ -82,18 +82,6 @@ from langchain_core.runnables.utils import (
|
||||
is_async_generator,
|
||||
)
|
||||
from langchain_core.tracers._streaming import _StreamingCallbackHandler
|
||||
from langchain_core.tracers.event_stream import (
|
||||
_astream_events_implementation_v1,
|
||||
_astream_events_implementation_v2,
|
||||
)
|
||||
from langchain_core.tracers.log_stream import (
|
||||
LogStreamCallbackHandler,
|
||||
_astream_log_implementation,
|
||||
)
|
||||
from langchain_core.tracers.root_listeners import (
|
||||
AsyncRootListenersTracer,
|
||||
RootListenersTracer,
|
||||
)
|
||||
from langchain_core.utils.aiter import aclosing, atee
|
||||
from langchain_core.utils.iter import safetee
|
||||
from langchain_core.utils.pydantic import create_model_v2
|
||||
@@ -111,8 +99,21 @@ if TYPE_CHECKING:
|
||||
from langchain_core.runnables.retry import ExponentialJitterParams
|
||||
from langchain_core.runnables.schema import StreamEvent
|
||||
from langchain_core.tools import BaseTool
|
||||
from langchain_core.tracers.log_stream import RunLog, RunLogPatch
|
||||
from langchain_core.tracers.root_listeners import AsyncListener
|
||||
from langchain_core.tracers.event_stream import (
|
||||
_astream_events_implementation_v1, # noqa: F401
|
||||
_astream_events_implementation_v2, # noqa: F401
|
||||
)
|
||||
from langchain_core.tracers.log_stream import (
|
||||
LogStreamCallbackHandler, # noqa: F401
|
||||
RunLog,
|
||||
RunLogPatch,
|
||||
_astream_log_implementation, # noqa: F401
|
||||
)
|
||||
from langchain_core.tracers.root_listeners import (
|
||||
AsyncListener,
|
||||
AsyncRootListenersTracer, # noqa: F401
|
||||
RootListenersTracer, # noqa: F401
|
||||
)
|
||||
from langchain_core.tracers.schemas import Run
|
||||
|
||||
|
||||
@@ -1245,6 +1246,11 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
A `RunLogPatch` or `RunLog` object.
|
||||
|
||||
"""
|
||||
from langchain_core.tracers.log_stream import ( # noqa: PLC0415
|
||||
LogStreamCallbackHandler,
|
||||
_astream_log_implementation,
|
||||
)
|
||||
|
||||
stream = LogStreamCallbackHandler(
|
||||
auto_close=False,
|
||||
include_names=include_names,
|
||||
@@ -1480,6 +1486,11 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
NotImplementedError: If the version is not `'v1'` or `'v2'`.
|
||||
|
||||
""" # noqa: E501
|
||||
from langchain_core.tracers.event_stream import ( # noqa: PLC0415
|
||||
_astream_events_implementation_v1,
|
||||
_astream_events_implementation_v2,
|
||||
)
|
||||
|
||||
if version == "v2":
|
||||
event_stream = _astream_events_implementation_v2(
|
||||
self,
|
||||
@@ -1722,6 +1733,10 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
chain.invoke(2)
|
||||
```
|
||||
"""
|
||||
from langchain_core.tracers.root_listeners import ( # noqa: PLC0415
|
||||
RootListenersTracer,
|
||||
)
|
||||
|
||||
return RunnableBinding(
|
||||
bound=self,
|
||||
config_factories=[
|
||||
@@ -1819,6 +1834,10 @@ class Runnable(ABC, Generic[Input, Output]):
|
||||
# on end callback ends at 2025-03-01T07:05:30.884831+00:00
|
||||
```
|
||||
"""
|
||||
from langchain_core.tracers.root_listeners import ( # noqa: PLC0415
|
||||
AsyncRootListenersTracer,
|
||||
)
|
||||
|
||||
return RunnableBinding(
|
||||
bound=self,
|
||||
config_factories=[
|
||||
@@ -6049,6 +6068,9 @@ class RunnableBinding(RunnableBindingBase[Input, Output]): # type: ignore[no-re
|
||||
Returns:
|
||||
A new `Runnable` with the listeners bound.
|
||||
"""
|
||||
from langchain_core.tracers.root_listeners import ( # noqa: PLC0415
|
||||
RootListenersTracer,
|
||||
)
|
||||
|
||||
def listener_config_factory(config: RunnableConfig) -> RunnableConfig:
|
||||
return {
|
||||
|
||||
@@ -7,15 +7,7 @@ import asyncio
|
||||
# Cannot move uuid to TYPE_CHECKING as RunnableConfig is used in Pydantic models
|
||||
import uuid # noqa: TC003
|
||||
import warnings
|
||||
from collections.abc import (
|
||||
Awaitable,
|
||||
Callable,
|
||||
Generator,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
Sequence,
|
||||
)
|
||||
from collections.abc import Awaitable, Callable, Generator, Iterable, Iterator, Sequence
|
||||
from concurrent.futures import Executor, Future, ThreadPoolExecutor
|
||||
from contextlib import contextmanager
|
||||
from contextvars import Context, ContextVar, Token, copy_context
|
||||
@@ -37,7 +29,6 @@ from langchain_core.runnables.utils import (
|
||||
accepts_config,
|
||||
accepts_run_manager,
|
||||
)
|
||||
from langchain_core.tracers._constants import LANGSMITH_INHERITABLE_METADATA_KEYS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langchain_core.callbacks.base import BaseCallbackManager, Callbacks
|
||||
@@ -155,30 +146,17 @@ CONFIGURABLE_TO_TRACING_METADATA_EXCLUDED_KEYS = frozenset(("api_key",))
|
||||
|
||||
def _get_langsmith_inheritable_metadata_from_config(
|
||||
config: RunnableConfig,
|
||||
*,
|
||||
extra_metadata: Mapping[str, Any] | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Get LangSmith-only inheritable metadata defaults derived from config.
|
||||
|
||||
Args:
|
||||
config: The config.
|
||||
extra_metadata: Additional LangSmith-only metadata to merge in (takes
|
||||
precedence over configurable-derived entries). Intended for entries
|
||||
already extracted from ``config["metadata"]`` by the caller so we
|
||||
don't iterate over it twice.
|
||||
"""
|
||||
"""Get LangSmith-only inheritable metadata defaults derived from config."""
|
||||
configurable = config.get("configurable") or {}
|
||||
config_metadata = config.get("metadata") or {}
|
||||
metadata = {
|
||||
key: value
|
||||
for key, value in configurable.items()
|
||||
if not key.startswith("__")
|
||||
and isinstance(value, (str, int, float, bool))
|
||||
and key not in config_metadata
|
||||
and key not in config.get("metadata", {})
|
||||
and key not in CONFIGURABLE_TO_TRACING_METADATA_EXCLUDED_KEYS
|
||||
}
|
||||
if extra_metadata:
|
||||
metadata.update(extra_metadata)
|
||||
return metadata or None
|
||||
|
||||
|
||||
@@ -542,36 +520,6 @@ def acall_func_with_variable_args(
|
||||
return func(input, **kwargs) # type: ignore[call-arg]
|
||||
|
||||
|
||||
def _split_inheritable_metadata(
|
||||
config: RunnableConfig,
|
||||
) -> tuple[dict[str, Any] | None, dict[str, Any]]:
|
||||
"""Split ``config["metadata"]`` into general and LangSmith-only halves.
|
||||
|
||||
Allowlisted keys (see
|
||||
:data:`langchain_core.tracers._constants.LANGSMITH_INHERITABLE_METADATA_KEYS`)
|
||||
are *moved* out of the general inheritable metadata so they don't reach
|
||||
non-tracer callback handlers (e.g. ``stream_events`` output). Nested
|
||||
override semantics are preserved on the tracer side by
|
||||
``LangChainTracer.copy_with_metadata_defaults``, which treats the same
|
||||
allowlist as last-wins.
|
||||
|
||||
Returns a tuple of ``(general_metadata, langsmith_only_metadata)``. The
|
||||
first element preserves ``None`` when the config has no ``metadata`` key
|
||||
so callers can forward it unchanged to ``CallbackManager.configure``.
|
||||
"""
|
||||
metadata = config.get("metadata")
|
||||
if metadata is None:
|
||||
return None, {}
|
||||
general: dict[str, Any] = {}
|
||||
langsmith_only: dict[str, Any] = {}
|
||||
for key, value in metadata.items():
|
||||
if key in LANGSMITH_INHERITABLE_METADATA_KEYS:
|
||||
langsmith_only[key] = value
|
||||
else:
|
||||
general[key] = value
|
||||
return general, langsmith_only
|
||||
|
||||
|
||||
def get_callback_manager_for_config(config: RunnableConfig) -> CallbackManager:
|
||||
"""Get a callback manager for a config.
|
||||
|
||||
@@ -581,13 +529,12 @@ def get_callback_manager_for_config(config: RunnableConfig) -> CallbackManager:
|
||||
Returns:
|
||||
The callback manager.
|
||||
"""
|
||||
general_metadata, langsmith_only_metadata = _split_inheritable_metadata(config)
|
||||
return CallbackManager.configure(
|
||||
inheritable_callbacks=config.get("callbacks"),
|
||||
inheritable_tags=config.get("tags"),
|
||||
inheritable_metadata=general_metadata,
|
||||
inheritable_metadata=config.get("metadata"),
|
||||
langsmith_inheritable_metadata=_get_langsmith_inheritable_metadata_from_config(
|
||||
config, extra_metadata=langsmith_only_metadata
|
||||
config
|
||||
),
|
||||
)
|
||||
|
||||
@@ -603,13 +550,12 @@ def get_async_callback_manager_for_config(
|
||||
Returns:
|
||||
The async callback manager.
|
||||
"""
|
||||
general_metadata, langsmith_only_metadata = _split_inheritable_metadata(config)
|
||||
return AsyncCallbackManager.configure(
|
||||
inheritable_callbacks=config.get("callbacks"),
|
||||
inheritable_tags=config.get("tags"),
|
||||
inheritable_metadata=general_metadata,
|
||||
inheritable_metadata=config.get("metadata"),
|
||||
langsmith_inheritable_metadata=_get_langsmith_inheritable_metadata_from_config(
|
||||
config, extra_metadata=langsmith_only_metadata
|
||||
config
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -552,8 +552,15 @@ class ChildTool(BaseTool):
|
||||
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
ignored_types=(functools.cached_property,),
|
||||
)
|
||||
|
||||
def __setattr__(self, name: str, value: Any) -> None:
|
||||
super().__setattr__(name, value)
|
||||
if name in {"args_schema", "description", "name"}:
|
||||
self.__dict__.pop("tool_call_schema", None)
|
||||
self.__dict__.pop("args", None)
|
||||
|
||||
@property
|
||||
def is_single_input(self) -> bool:
|
||||
"""Check if the tool accepts only a single input argument.
|
||||
@@ -564,7 +571,7 @@ class ChildTool(BaseTool):
|
||||
keys = {k for k in self.args if k != "kwargs"}
|
||||
return len(keys) == 1
|
||||
|
||||
@property
|
||||
@functools.cached_property
|
||||
def args(self) -> dict:
|
||||
"""Get the tool's input arguments schema.
|
||||
|
||||
@@ -583,7 +590,7 @@ class ChildTool(BaseTool):
|
||||
json_schema = input_schema.model_json_schema()
|
||||
return cast("dict", json_schema["properties"])
|
||||
|
||||
@property
|
||||
@functools.cached_property
|
||||
def tool_call_schema(self) -> ArgsSchema:
|
||||
"""Get the schema for tool calls, excluding injected arguments.
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Private constants shared across tracer and runnable-config code.
|
||||
|
||||
These live in their own module so they can be imported without pulling in
|
||||
the heavier ``langchain_core.tracers.langchain`` dependency (which loads
|
||||
``langsmith`` transitively).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
LANGSMITH_INHERITABLE_METADATA_KEYS: frozenset[str] = frozenset(("ls_agent_type",))
|
||||
"""Allowlist of metadata keys routed to LangSmith tracers only.
|
||||
|
||||
Keys in this set are:
|
||||
|
||||
1. Stripped from general ``inheritable_metadata`` by
|
||||
``langchain_core.runnables.config._split_inheritable_metadata`` so they
|
||||
don't reach non-tracer callback handlers (``stream_events``,
|
||||
``astream_log``, user-provided ``BaseCallbackHandler`` instances, etc.).
|
||||
2. Forwarded to ``LangChainTracer`` as *overridable* defaults via
|
||||
``LangChainTracer.copy_with_metadata_defaults``. Unlike general
|
||||
metadata defaults (first-wins), keys in this allowlist are last-wins so
|
||||
that a nested ``RunnableConfig`` / ``CallbackManager.configure`` call
|
||||
can rescope the value to the innermost run (e.g. ``ls_agent_type``).
|
||||
"""
|
||||
# TODO: Expand this to cover all `ls_`-prefixed metadata keys.
|
||||
@@ -23,7 +23,6 @@ from langchain_core.env import get_runtime_environment
|
||||
from langchain_core.load import dumpd
|
||||
from langchain_core.messages.ai import UsageMetadata, add_usage
|
||||
from langchain_core.tracers._compat import run_construct, run_to_dict
|
||||
from langchain_core.tracers._constants import LANGSMITH_INHERITABLE_METADATA_KEYS
|
||||
from langchain_core.tracers.base import BaseTracer
|
||||
from langchain_core.tracers.schemas import Run
|
||||
|
||||
@@ -168,16 +167,7 @@ class LangChainTracer(BaseTracer):
|
||||
metadata: Mapping[str, str] | None = None,
|
||||
tags: list[str] | None = None,
|
||||
) -> LangChainTracer:
|
||||
"""Return a new tracer with merged tracer-only defaults.
|
||||
|
||||
By default, keys already present on this tracer take precedence over
|
||||
``metadata`` (first-wins). Keys in
|
||||
:data:`langchain_core.tracers._constants.LANGSMITH_INHERITABLE_METADATA_KEYS`
|
||||
are the exception: they are treated as narrowly scoped, overridable
|
||||
defaults so that a nested ``RunnableConfig`` /
|
||||
``CallbackManager.configure`` call can rescope them to the innermost
|
||||
run (e.g. ``ls_agent_type``).
|
||||
"""
|
||||
"""Return a new tracer with merged tracer-only defaults."""
|
||||
base_metadata = self.tracing_metadata
|
||||
if metadata is None:
|
||||
merged_metadata = dict(base_metadata) if base_metadata is not None else None
|
||||
@@ -186,9 +176,7 @@ class LangChainTracer(BaseTracer):
|
||||
else:
|
||||
merged_metadata = dict(base_metadata)
|
||||
for key, value in metadata.items():
|
||||
if key in LANGSMITH_INHERITABLE_METADATA_KEYS or (
|
||||
key not in merged_metadata
|
||||
):
|
||||
if key not in merged_metadata:
|
||||
merged_metadata[key] = value
|
||||
|
||||
merged_tags = sorted(set(self.tags + tags)) if tags else self.tags
|
||||
|
||||
@@ -340,17 +340,18 @@ def _format_tool_to_openai_function(tool: BaseTool) -> FunctionDescription:
|
||||
is_simple_oai_tool = (
|
||||
isinstance(tool, langchain_core.tools.simple.Tool) and not tool.args_schema
|
||||
)
|
||||
if tool.tool_call_schema and not is_simple_oai_tool:
|
||||
if isinstance(tool.tool_call_schema, dict):
|
||||
schema = tool.tool_call_schema
|
||||
if schema and not is_simple_oai_tool:
|
||||
if isinstance(schema, dict):
|
||||
return _convert_json_schema_to_openai_function(
|
||||
tool.tool_call_schema, name=tool.name, description=tool.description
|
||||
schema, name=tool.name, description=tool.description
|
||||
)
|
||||
if issubclass(tool.tool_call_schema, (BaseModel, BaseModelV1)):
|
||||
if issubclass(schema, (BaseModel, BaseModelV1)):
|
||||
return _convert_pydantic_to_openai_function(
|
||||
tool.tool_call_schema, name=tool.name, description=tool.description
|
||||
schema, name=tool.name, description=tool.description
|
||||
)
|
||||
error_msg = (
|
||||
f"Unsupported tool call schema: {tool.tool_call_schema}. "
|
||||
f"Unsupported tool call schema: {schema}. "
|
||||
"Tool call schema must be a JSON schema dict or a Pydantic model."
|
||||
)
|
||||
raise ValueError(error_msg)
|
||||
|
||||
@@ -199,18 +199,21 @@ class _IgnoreUnserializable(GenerateJsonSchema):
|
||||
return {}
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def _create_subset_model_v1(
|
||||
name: str,
|
||||
model: type[BaseModelV1],
|
||||
field_names: list,
|
||||
field_names: tuple[str, ...],
|
||||
*,
|
||||
descriptions: dict | None = None,
|
||||
descriptions: tuple[tuple[str, str], ...] | None = None,
|
||||
fn_description: str | None = None,
|
||||
) -> type[BaseModelV1]:
|
||||
"""Create a Pydantic model with only a subset of model's fields."""
|
||||
descriptions_ = dict(descriptions) if descriptions else {}
|
||||
field_names_list = list(field_names)
|
||||
fields = {}
|
||||
|
||||
for field_name in field_names:
|
||||
for field_name in field_names_list:
|
||||
# Using pydantic v1 so can access __fields__ as a dict.
|
||||
field = model.__fields__[field_name]
|
||||
t = (
|
||||
@@ -219,8 +222,8 @@ def _create_subset_model_v1(
|
||||
if field.required and not field.allow_none
|
||||
else field.outer_type_ | None
|
||||
)
|
||||
if descriptions and field_name in descriptions:
|
||||
field.field_info.description = descriptions[field_name]
|
||||
if descriptions_ and field_name in descriptions_:
|
||||
field.field_info.description = descriptions_[field_name]
|
||||
fields[field_name] = (t, field.field_info)
|
||||
|
||||
rtn = cast("type[BaseModelV1]", create_model_v1(name, **fields)) # type: ignore[call-overload]
|
||||
@@ -228,18 +231,20 @@ def _create_subset_model_v1(
|
||||
return rtn
|
||||
|
||||
|
||||
@lru_cache(maxsize=256)
|
||||
def _create_subset_model_v2(
|
||||
name: str,
|
||||
model: type[BaseModel],
|
||||
field_names: list[str],
|
||||
field_names: tuple[str, ...],
|
||||
*,
|
||||
descriptions: dict | None = None,
|
||||
descriptions: tuple[tuple[str, str], ...] | None = None,
|
||||
fn_description: str | None = None,
|
||||
) -> type[BaseModel]:
|
||||
"""Create a Pydantic model with a subset of the model fields."""
|
||||
descriptions_ = descriptions or {}
|
||||
descriptions_ = dict(descriptions) if descriptions else {}
|
||||
field_names_list = list(field_names)
|
||||
fields = {}
|
||||
for field_name in field_names:
|
||||
for field_name in field_names_list:
|
||||
field = model.model_fields[field_name]
|
||||
description = descriptions_.get(field_name, field.description)
|
||||
field_kwargs: dict[str, Any] = {"description": description}
|
||||
@@ -291,19 +296,20 @@ def _create_subset_model(
|
||||
Returns:
|
||||
The created subset model.
|
||||
"""
|
||||
descriptions_tuple = tuple(descriptions.items()) if descriptions else None
|
||||
if issubclass(model, BaseModelV1):
|
||||
return _create_subset_model_v1(
|
||||
name,
|
||||
model,
|
||||
field_names,
|
||||
descriptions=descriptions,
|
||||
tuple(field_names),
|
||||
descriptions=descriptions_tuple,
|
||||
fn_description=fn_description,
|
||||
)
|
||||
return _create_subset_model_v2(
|
||||
name,
|
||||
model,
|
||||
field_names,
|
||||
descriptions=descriptions,
|
||||
tuple(field_names),
|
||||
descriptions=descriptions_tuple,
|
||||
fn_description=fn_description,
|
||||
)
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ from langchain_core.runnables.config import (
|
||||
_get_langsmith_inheritable_metadata_from_config,
|
||||
_set_config_context,
|
||||
ensure_config,
|
||||
get_async_callback_manager_for_config,
|
||||
get_callback_manager_for_config,
|
||||
merge_configs,
|
||||
run_in_executor,
|
||||
)
|
||||
@@ -232,101 +230,6 @@ def test_get_langsmith_inheritable_metadata_from_config_uses_previous_copy_rules
|
||||
}
|
||||
|
||||
|
||||
def test_get_langsmith_inheritable_metadata_pulls_allowlisted_metadata() -> None:
|
||||
# Callers pre-extract allowlisted metadata and pass it via `extra_metadata`
|
||||
# so that `config["metadata"]` is only iterated once.
|
||||
config: RunnableConfig = {
|
||||
"metadata": {
|
||||
"foo": "bar",
|
||||
"ls_agent_type": "react",
|
||||
"ls_provider": "openai", # not on the allowlist
|
||||
},
|
||||
"configurable": {"baz": "qux"},
|
||||
}
|
||||
|
||||
assert _get_langsmith_inheritable_metadata_from_config(
|
||||
config, extra_metadata={"ls_agent_type": "react"}
|
||||
) == {
|
||||
"baz": "qux",
|
||||
"ls_agent_type": "react",
|
||||
}
|
||||
|
||||
|
||||
def test_get_langsmith_inheritable_metadata_handles_missing_metadata() -> None:
|
||||
# No metadata key at all
|
||||
assert _get_langsmith_inheritable_metadata_from_config(
|
||||
{"configurable": {"baz": "qux"}}
|
||||
) == {"baz": "qux"}
|
||||
|
||||
# metadata present but no allowlisted keys and no configurable contributions
|
||||
assert (
|
||||
_get_langsmith_inheritable_metadata_from_config({"metadata": {"foo": "bar"}})
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_get_langsmith_inheritable_metadata_extra_metadata_overrides_configurable() -> (
|
||||
None
|
||||
):
|
||||
# `extra_metadata` takes precedence over configurable entries with the
|
||||
# same name (applied after the initial dict is built). The existing
|
||||
# `key not in config_metadata` guard in the configurable pass also
|
||||
# prevents configurable entries from shadowing metadata keys that the
|
||||
# caller intentionally routed into `extra_metadata`.
|
||||
config: RunnableConfig = {
|
||||
"metadata": {"ls_agent_type": "from-metadata"},
|
||||
"configurable": {"ls_agent_type": "from-configurable"},
|
||||
}
|
||||
|
||||
assert _get_langsmith_inheritable_metadata_from_config(
|
||||
config, extra_metadata={"ls_agent_type": "from-metadata"}
|
||||
) == {
|
||||
"ls_agent_type": "from-metadata",
|
||||
}
|
||||
|
||||
|
||||
def test_get_callback_manager_for_config_filters_allowlisted_metadata() -> None:
|
||||
# Allowlisted keys (LangSmith-only) are stripped from general inheritable
|
||||
# metadata so they don't reach non-tracer callback handlers or
|
||||
# `stream_events` output. They reach tracers via the
|
||||
# `langsmith_inheritable_metadata` path instead.
|
||||
config: RunnableConfig = {
|
||||
"metadata": {
|
||||
"foo": "bar",
|
||||
"ls_agent_type": "react",
|
||||
"ls_provider": "openai", # not on the allowlist
|
||||
},
|
||||
}
|
||||
|
||||
manager = get_callback_manager_for_config(config)
|
||||
|
||||
# Allowlisted keys are stripped; regular keys (including non-allowlisted
|
||||
# ls_* keys) pass through.
|
||||
assert manager.inheritable_metadata == {"foo": "bar", "ls_provider": "openai"}
|
||||
|
||||
|
||||
def test_get_callback_manager_for_config_preserves_empty_metadata() -> None:
|
||||
# When no metadata is supplied, inheritable_metadata should remain empty
|
||||
# (and the split should not raise on a missing metadata key).
|
||||
manager = get_callback_manager_for_config({})
|
||||
assert not manager.inheritable_metadata
|
||||
|
||||
|
||||
async def test_get_async_callback_manager_for_config_filters_allowlisted_metadata() -> (
|
||||
None
|
||||
):
|
||||
config: RunnableConfig = {
|
||||
"metadata": {
|
||||
"foo": "bar",
|
||||
"ls_agent_type": "react",
|
||||
},
|
||||
}
|
||||
|
||||
manager = get_async_callback_manager_for_config(config)
|
||||
|
||||
assert manager.inheritable_metadata == {"foo": "bar"}
|
||||
|
||||
|
||||
async def test_merge_config_callbacks() -> None:
|
||||
manager: RunnableConfig = {
|
||||
"callbacks": CallbackManager(handlers=[StdOutCallbackHandler()])
|
||||
|
||||
@@ -1040,95 +1040,6 @@ class TestLangsmithInheritableTracingDefaultsInConfigure:
|
||||
md = posts[0].get("extra", {}).get("metadata", {})
|
||||
assert md["env"] == "prod"
|
||||
|
||||
def test_nested_ls_agent_type_is_scoped_to_each_runnable(self) -> None:
|
||||
"""Allowlisted `ls_` metadata set on a nested runnable overrides the outer.
|
||||
|
||||
An outer runnable bound with `ls_agent_type="root"` that invokes an
|
||||
inner runnable bound with `ls_agent_type="subagent"` should post two
|
||||
runs where each carries its own `ls_agent_type`. In particular, the
|
||||
inner run's metadata must NOT inherit `"root"` from the outer.
|
||||
"""
|
||||
tracer = _create_tracer_with_mocked_client()
|
||||
|
||||
@RunnableLambda
|
||||
def inner(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
inner_with_config = inner.with_config(
|
||||
{"metadata": {"ls_agent_type": "subagent"}, "run_name": "inner"}
|
||||
)
|
||||
|
||||
@RunnableLambda
|
||||
def outer(x: int) -> int:
|
||||
return inner_with_config.invoke(x)
|
||||
|
||||
outer_with_config = outer.with_config(
|
||||
{"metadata": {"ls_agent_type": "root"}, "run_name": "outer"}
|
||||
)
|
||||
|
||||
outer_with_config.invoke(1, {"callbacks": [tracer]})
|
||||
|
||||
posts = _get_posts(tracer.client)
|
||||
posts_by_name = {post.get("name"): post for post in posts}
|
||||
assert set(posts_by_name) >= {"outer", "inner"}, (
|
||||
f"expected both outer and inner runs, got {list(posts_by_name)}"
|
||||
)
|
||||
|
||||
outer_md = posts_by_name["outer"].get("extra", {}).get("metadata", {})
|
||||
inner_md = posts_by_name["inner"].get("extra", {}).get("metadata", {})
|
||||
|
||||
assert outer_md.get("ls_agent_type") == "root"
|
||||
assert inner_md.get("ls_agent_type") == "subagent"
|
||||
|
||||
def test_ls_agent_type_not_visible_to_non_tracer_handlers(self) -> None:
|
||||
"""Allowlisted `ls_` metadata is tracer-only.
|
||||
|
||||
It must not reach non-tracer callback handlers (which would surface
|
||||
it in ``stream_events``, ``astream_log``, and user-provided handlers).
|
||||
"""
|
||||
seen_metadata: list[dict[str, Any]] = []
|
||||
|
||||
class RecordingHandler(BaseCallbackHandler):
|
||||
def on_chain_start(
|
||||
self,
|
||||
serialized: dict[str, Any] | None,
|
||||
inputs: dict[str, Any] | Any,
|
||||
*,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
seen_metadata.append(dict(metadata or {}))
|
||||
|
||||
@RunnableLambda
|
||||
def inner(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
inner_with_config = inner.with_config(
|
||||
{"metadata": {"ls_agent_type": "subagent", "visible": "inner"}}
|
||||
)
|
||||
|
||||
@RunnableLambda
|
||||
def outer(x: int) -> int:
|
||||
return inner_with_config.invoke(x)
|
||||
|
||||
outer_with_config = outer.with_config(
|
||||
{"metadata": {"ls_agent_type": "root", "visible": "outer"}}
|
||||
)
|
||||
|
||||
outer_with_config.invoke(1, {"callbacks": [RecordingHandler()]})
|
||||
|
||||
# Every non-tracer callback invocation must exclude `ls_agent_type`.
|
||||
assert seen_metadata, "expected on_chain_start to fire"
|
||||
for md in seen_metadata:
|
||||
assert "ls_agent_type" not in md, (
|
||||
f"ls_agent_type leaked to non-tracer handler: {md}"
|
||||
)
|
||||
|
||||
# Regular metadata keys should still be visible so the filter is
|
||||
# correctly scoped to the allowlist.
|
||||
assert any(md.get("visible") == "outer" for md in seen_metadata)
|
||||
assert any(md.get("visible") == "inner" for md in seen_metadata)
|
||||
|
||||
def test_runnable_config_copies_configurable_values_to_tracing_metadata(
|
||||
self,
|
||||
) -> None:
|
||||
|
||||
@@ -3653,3 +3653,42 @@ def test_tool_default_factory_not_required() -> None:
|
||||
schema = convert_to_openai_tool(some_func)
|
||||
params = schema["function"]["parameters"]
|
||||
assert "names" not in params.get("required", [])
|
||||
|
||||
|
||||
def test_tool_call_schema_cache_invalidated_on_field_mutation() -> None:
|
||||
"""Verify cached tool_call_schema is invalidated when schema-affecting fields change."""
|
||||
|
||||
@tool
|
||||
def my_tool(x: int) -> str:
|
||||
"""Original description."""
|
||||
return str(x)
|
||||
|
||||
schema_before = my_tool.tool_call_schema
|
||||
my_tool.description = "Updated description"
|
||||
schema_after = my_tool.tool_call_schema
|
||||
|
||||
assert schema_before is not schema_after
|
||||
assert schema_after.__doc__ == "Updated description"
|
||||
|
||||
|
||||
def test_args_cache_invalidated_on_args_schema_change() -> None:
|
||||
"""Verify cached args is invalidated when args_schema changes."""
|
||||
|
||||
class SchemaA(BaseModel):
|
||||
x: int
|
||||
|
||||
class SchemaB(BaseModel):
|
||||
y: str
|
||||
|
||||
@tool(args_schema=SchemaA)
|
||||
def my_tool(x: int) -> str:
|
||||
"""A tool."""
|
||||
return str(x)
|
||||
|
||||
args_before = my_tool.args
|
||||
assert "x" in args_before
|
||||
|
||||
my_tool.args_schema = SchemaB
|
||||
args_after = my_tool.args
|
||||
assert "y" in args_after
|
||||
assert "x" not in args_after
|
||||
|
||||
@@ -115,7 +115,7 @@ def test_with_field_metadata() -> None:
|
||||
description="List of integers", min_length=10, max_length=15
|
||||
)
|
||||
|
||||
subset_model = _create_subset_model_v2("Foo", Foo, ["x"])
|
||||
subset_model = _create_subset_model_v2("Foo", Foo, ("x",))
|
||||
assert subset_model.model_json_schema() == {
|
||||
"properties": {
|
||||
"x": {
|
||||
@@ -199,7 +199,7 @@ def test_create_subset_model_v2_preserves_default_factory() -> None:
|
||||
subset = _create_subset_model_v2(
|
||||
"Subset",
|
||||
Original,
|
||||
["required_field", "names", "mapping"],
|
||||
("required_field", "names", "mapping"),
|
||||
)
|
||||
schema = subset.model_json_schema()
|
||||
assert schema.get("required") == ["required_field"]
|
||||
|
||||
@@ -69,6 +69,7 @@ test = [
|
||||
"pytest-xdist<4.0.0,>=3.6.1",
|
||||
"pytest-mock",
|
||||
"pytest-benchmark>=5.1.0,<6.0.0",
|
||||
"pytest-codspeed>=3.0.0,<4.0.0",
|
||||
"syrupy>=5.0.0,<6.0.0",
|
||||
"toml>=0.10.2,<1.0.0",
|
||||
"blockbuster>=1.5.26,<1.6.0",
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tracemalloc
|
||||
from itertools import cycle
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import pytest
|
||||
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
|
||||
from langchain_core.messages import AIMessage
|
||||
from langchain_core.tools import tool
|
||||
from pydantic import BaseModel, Field
|
||||
from pytest_benchmark.fixture import BenchmarkFixture
|
||||
|
||||
from langchain.agents import create_agent
|
||||
@@ -18,27 +24,314 @@ if TYPE_CHECKING:
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool fixtures — a realistic mix of simple, structured, and nested schemas
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@tool
|
||||
def simple_tool_1(x: int) -> str:
|
||||
"""Add one to a number."""
|
||||
return str(x + 1)
|
||||
|
||||
|
||||
@tool
|
||||
def simple_tool_2(text: str) -> str:
|
||||
"""Reverse a string."""
|
||||
return text[::-1]
|
||||
|
||||
|
||||
@tool
|
||||
def simple_tool_3(a: float, b: float) -> float:
|
||||
"""Multiply two numbers."""
|
||||
return a * b
|
||||
|
||||
|
||||
@tool
|
||||
def simple_tool_4(items: list[str]) -> int:
|
||||
"""Count items in a list."""
|
||||
return len(items)
|
||||
|
||||
|
||||
@tool
|
||||
def simple_tool_5(flag: bool, value: str) -> str:
|
||||
"""Return value if flag is true."""
|
||||
return value if flag else ""
|
||||
|
||||
|
||||
class AddressSchema(BaseModel):
|
||||
street: str = Field(description="Street address")
|
||||
city: str = Field(description="City name")
|
||||
zip_code: str = Field(description="ZIP code")
|
||||
|
||||
|
||||
class PersonSchema(BaseModel):
|
||||
name: str = Field(description="Full name")
|
||||
age: int = Field(description="Age in years")
|
||||
address: AddressSchema = Field(description="Home address")
|
||||
tags: list[str] = Field(default_factory=list, description="Tags")
|
||||
|
||||
|
||||
@tool(args_schema=PersonSchema)
|
||||
def structured_tool_1(name: str, age: int, address: AddressSchema, tags: list[str]) -> str:
|
||||
"""Look up a person by their details."""
|
||||
return f"{name}, {age}"
|
||||
|
||||
|
||||
class SearchSchema(BaseModel):
|
||||
query: str = Field(description="Search query string")
|
||||
max_results: int = Field(default=10, description="Maximum results to return")
|
||||
filters: dict[str, str] = Field(default_factory=dict, description="Filter map")
|
||||
|
||||
|
||||
@tool(args_schema=SearchSchema)
|
||||
def structured_tool_2(query: str, max_results: int, filters: dict[str, str]) -> list[str]:
|
||||
"""Search a database with filters."""
|
||||
return [query] * min(max_results, 3)
|
||||
|
||||
|
||||
class FileSchema(BaseModel):
|
||||
path: str = Field(description="File path")
|
||||
encoding: str = Field(default="utf-8", description="File encoding")
|
||||
lines: list[int] = Field(default_factory=list, description="Line numbers to read")
|
||||
|
||||
|
||||
@tool(args_schema=FileSchema)
|
||||
def structured_tool_3(path: str, encoding: str, lines: list[int]) -> str:
|
||||
"""Read a file at a given path."""
|
||||
return f"content of {path}"
|
||||
|
||||
|
||||
class MatrixSchema(BaseModel):
|
||||
rows: int = Field(description="Number of rows")
|
||||
cols: int = Field(description="Number of columns")
|
||||
fill: float = Field(default=0.0, description="Fill value")
|
||||
|
||||
|
||||
@tool(args_schema=MatrixSchema)
|
||||
def structured_tool_4(rows: int, cols: int, fill: float) -> list[list[float]]:
|
||||
"""Create a matrix filled with a value."""
|
||||
return [[fill] * cols for _ in range(rows)]
|
||||
|
||||
|
||||
class CoordinateSchema(BaseModel):
|
||||
lat: float = Field(description="Latitude")
|
||||
lon: float = Field(description="Longitude")
|
||||
|
||||
|
||||
class LocationSchema(BaseModel):
|
||||
name: str = Field(description="Location name")
|
||||
coordinate: CoordinateSchema = Field(description="GPS coordinate")
|
||||
altitude_m: float = Field(default=0.0, description="Altitude in meters")
|
||||
|
||||
|
||||
class RouteSchema(BaseModel):
|
||||
origin: LocationSchema = Field(description="Starting location")
|
||||
destination: LocationSchema = Field(description="Ending location")
|
||||
waypoints: list[LocationSchema] = Field(default_factory=list, description="Intermediate stops")
|
||||
max_distance_km: float = Field(default=1000.0, description="Maximum route distance")
|
||||
|
||||
|
||||
@tool(args_schema=RouteSchema)
|
||||
def deep_nested_tool(
|
||||
origin: LocationSchema,
|
||||
destination: LocationSchema,
|
||||
waypoints: list[LocationSchema],
|
||||
max_distance_km: float,
|
||||
) -> dict[str, Any]:
|
||||
"""Plan a route between locations with deep nested schema."""
|
||||
return {"origin": origin.name, "destination": destination.name}
|
||||
|
||||
|
||||
@tool
|
||||
def complex_tool_1(
|
||||
name: str,
|
||||
metadata: dict[str, Any],
|
||||
tags: list[str],
|
||||
priority: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
"""Create an item with metadata."""
|
||||
return {"name": name, "metadata": metadata, "tags": tags, "priority": priority}
|
||||
|
||||
|
||||
@tool
|
||||
def complex_tool_2(
|
||||
source: str,
|
||||
destination: str,
|
||||
options: dict[str, str] | None = None,
|
||||
) -> bool:
|
||||
"""Copy data from source to destination."""
|
||||
return True
|
||||
|
||||
|
||||
@tool
|
||||
def complex_tool_3(
|
||||
ids: list[int],
|
||||
batch_size: int = 10,
|
||||
retry: bool = False,
|
||||
) -> dict[str, list[int]]:
|
||||
"""Process a batch of IDs."""
|
||||
return {"processed": ids[:batch_size]}
|
||||
|
||||
|
||||
@tool
|
||||
def complex_tool_4(
|
||||
expression: str,
|
||||
variables: dict[str, float],
|
||||
precision: int = 6,
|
||||
) -> float:
|
||||
"""Evaluate a mathematical expression."""
|
||||
return 0.0
|
||||
|
||||
|
||||
@tool
|
||||
def complex_tool_5(
|
||||
url: str,
|
||||
method: str = "GET",
|
||||
headers: dict[str, str] | None = None,
|
||||
body: str | None = None,
|
||||
timeout: int = 30,
|
||||
) -> dict[str, Any]:
|
||||
"""Make an HTTP request."""
|
||||
return {"status": 200, "body": ""}
|
||||
|
||||
|
||||
SMALL_TOOLS = [simple_tool_1, simple_tool_2, simple_tool_3]
|
||||
MEDIUM_TOOLS = [
|
||||
simple_tool_1,
|
||||
simple_tool_2,
|
||||
simple_tool_3,
|
||||
simple_tool_4,
|
||||
simple_tool_5,
|
||||
structured_tool_1,
|
||||
structured_tool_2,
|
||||
]
|
||||
LARGE_TOOLS = [
|
||||
simple_tool_1,
|
||||
simple_tool_2,
|
||||
simple_tool_3,
|
||||
simple_tool_4,
|
||||
simple_tool_5,
|
||||
structured_tool_1,
|
||||
structured_tool_2,
|
||||
structured_tool_3,
|
||||
structured_tool_4,
|
||||
deep_nested_tool,
|
||||
complex_tool_1,
|
||||
complex_tool_2,
|
||||
complex_tool_3,
|
||||
complex_tool_4,
|
||||
complex_tool_5,
|
||||
]
|
||||
|
||||
|
||||
def _make_model() -> GenericFakeChatModel:
|
||||
return GenericFakeChatModel(messages=cycle([AIMessage(content="ok")]))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Benchmarks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_create_agent_instantiation(benchmark: BenchmarkFixture) -> None:
|
||||
def instantiate_agent() -> None:
|
||||
create_agent(model=GenericFakeChatModel(messages=iter([AIMessage(content="ok")])))
|
||||
|
||||
benchmark(instantiate_agent)
|
||||
"""Baseline: no tools."""
|
||||
benchmark(lambda: create_agent(model=_make_model()))
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_create_agent_instantiation_with_middleware(
|
||||
benchmark: BenchmarkFixture,
|
||||
) -> None:
|
||||
def instantiate_agent() -> None:
|
||||
def test_create_agent_small_tools(benchmark: BenchmarkFixture) -> None:
|
||||
"""3 simple tools."""
|
||||
benchmark(lambda: create_agent(model=_make_model(), tools=SMALL_TOOLS))
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_create_agent_medium_tools(benchmark: BenchmarkFixture) -> None:
|
||||
"""7 mixed tools."""
|
||||
benchmark(lambda: create_agent(model=_make_model(), tools=MEDIUM_TOOLS))
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_create_agent_large_tools(benchmark: BenchmarkFixture) -> None:
|
||||
"""15 tools including complex nested schemas."""
|
||||
benchmark(lambda: create_agent(model=_make_model(), tools=LARGE_TOOLS))
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_create_agent_large_tools_with_middleware(benchmark: BenchmarkFixture) -> None:
|
||||
"""15 tools + full middleware stack."""
|
||||
def run() -> None:
|
||||
middleware: Sequence[AgentMiddleware[Any, Any]] = (
|
||||
TodoListMiddleware(),
|
||||
ToolRetryMiddleware(),
|
||||
ModelRetryMiddleware(),
|
||||
)
|
||||
create_agent(
|
||||
model=GenericFakeChatModel(messages=iter([AIMessage(content="ok")])),
|
||||
model=_make_model(),
|
||||
tools=LARGE_TOOLS,
|
||||
middleware=middleware,
|
||||
)
|
||||
|
||||
benchmark(instantiate_agent)
|
||||
benchmark(run)
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_tool_call_schema_repeated_access(benchmark: BenchmarkFixture) -> None:
|
||||
"""Measure cost of repeated .tool_call_schema access on a complex tool (10 accesses per iteration)."""
|
||||
t = structured_tool_1
|
||||
|
||||
def access_schema_10x() -> None:
|
||||
for _ in range(10):
|
||||
t.tool_call_schema
|
||||
|
||||
benchmark(access_schema_10x)
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_tool_args_repeated_access(benchmark: BenchmarkFixture) -> None:
|
||||
"""Measure cost of repeated .args access on a complex tool (10 accesses per iteration)."""
|
||||
t = structured_tool_1
|
||||
|
||||
def access_args_10x() -> None:
|
||||
for _ in range(10):
|
||||
t.args
|
||||
|
||||
benchmark(access_args_10x)
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_create_agent_instantiation_with_middleware(benchmark: BenchmarkFixture) -> None:
|
||||
"""Baseline with middleware, no tools."""
|
||||
def run() -> None:
|
||||
middleware: Sequence[AgentMiddleware[Any, Any]] = (
|
||||
TodoListMiddleware(),
|
||||
ToolRetryMiddleware(),
|
||||
ModelRetryMiddleware(),
|
||||
)
|
||||
create_agent(model=_make_model(), middleware=middleware)
|
||||
|
||||
benchmark(run)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Memory snapshot (not a codspeed benchmark — uses tracemalloc directly)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.benchmark
|
||||
def test_create_agent_large_tools_memory() -> None:
|
||||
"""Observe peak memory for large-tools agent creation.
|
||||
|
||||
This is not a hard assertion — it records the tracemalloc peak for the
|
||||
memory allocated *during* create_agent. Run before and after optimization
|
||||
passes to track improvement. Update the printed baseline comment below
|
||||
when the number changes significantly.
|
||||
"""
|
||||
tracemalloc.start()
|
||||
create_agent(model=_make_model(), tools=LARGE_TOOLS)
|
||||
_, peak = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
peak_kb = peak / 1024
|
||||
# Baseline (pre-optimization): ~recorded after first run
|
||||
print(f"\nPeak memory during create_agent (15 tools): {peak_kb:.1f} KB")
|
||||
|
||||
33
libs/langchain_v1/uv.lock
generated
33
libs/langchain_v1/uv.lock
generated
@@ -1417,6 +1417,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/ed/6bfa4109fcb23a58819600392564fea69cdc6551ffd5e69ccf1d52a40cbc/greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", size = 271061, upload-time = "2025-08-07T13:17:15.373Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/fc/102ec1a2fc015b3a7652abab7acf3541d58c04d3d17a8d3d6a44adae1eb1/greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", size = 629475, upload-time = "2025-08-07T13:42:54.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/26/80383131d55a4ac0fb08d71660fd77e7660b9db6bdb4e8884f46d9f2cc04/greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", size = 640802, upload-time = "2025-08-07T13:45:25.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/7c/e7833dbcd8f376f3326bd728c845d31dcde4c84268d3921afcae77d90d08/greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", size = 636703, upload-time = "2025-08-07T13:53:12.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/49/547b93b7c0428ede7b3f309bc965986874759f7d89e4e04aeddbc9699acb/greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", size = 635417, upload-time = "2025-08-07T13:18:25.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/91/ae2eb6b7979e2f9b035a9f612cf70f1bf54aad4e1d125129bef1eae96f19/greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", size = 584358, upload-time = "2025-08-07T13:18:23.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/85/433de0c9c0252b22b16d413c9407e6cb3b41df7389afc366ca204dbc1393/greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", size = 1113550, upload-time = "2025-08-07T13:42:37.467Z" },
|
||||
@@ -1427,6 +1428,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/de/f28ced0a67749cac23fecb02b694f6473f47686dff6afaa211d186e2ef9c/greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", size = 272305, upload-time = "2025-08-07T13:15:41.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/16/2c3792cba130000bf2a31c5272999113f4764fd9d874fb257ff588ac779a/greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", size = 632472, upload-time = "2025-08-07T13:42:55.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/95d48d7e3d433e6dae5b1682e4292242a53f22df82e6d3dda81b1701a960/greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", size = 644646, upload-time = "2025-08-07T13:45:26.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/5e/405965351aef8c76b8ef7ad370e5da58d57ef6068df197548b015464001a/greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", size = 640519, upload-time = "2025-08-07T13:53:13.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/5d/382753b52006ce0218297ec1b628e048c4e64b155379331f25a7316eb749/greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", size = 639707, upload-time = "2025-08-07T13:18:27.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" },
|
||||
@@ -1437,6 +1439,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/16/035dcfcc48715ccd345f3a93183267167cdd162ad123cd93067d86f27ce4/greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968", size = 655185, upload-time = "2025-08-07T13:45:27.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/da/0386695eef69ffae1ad726881571dfe28b41970173947e7c558d9998de0f/greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", size = 649926, upload-time = "2025-08-07T13:53:15.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/88/69bf19fd4dc19981928ceacbc5fd4bb6bc2215d53199e367832e98d1d8fe/greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", size = 651839, upload-time = "2025-08-07T13:18:30.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
|
||||
@@ -1447,6 +1450,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
|
||||
@@ -1457,6 +1461,7 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
|
||||
@@ -1984,6 +1989,7 @@ test = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-benchmark" },
|
||||
{ name = "pytest-codspeed" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-socket" },
|
||||
@@ -2039,6 +2045,7 @@ test = [
|
||||
{ name = "pytest", specifier = ">=9.0.3,<10.0.0" },
|
||||
{ name = "pytest-asyncio", specifier = ">=1.3.0,<2.0.0" },
|
||||
{ name = "pytest-benchmark", specifier = ">=5.1.0,<6.0.0" },
|
||||
{ name = "pytest-codspeed", specifier = ">=3.0.0,<4.0.0" },
|
||||
{ name = "pytest-cov", specifier = ">=4.0.0,<8.0.0" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-socket", specifier = ">=0.6.0,<1.0.0" },
|
||||
@@ -4165,28 +4172,24 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-codspeed"
|
||||
version = "4.2.0"
|
||||
version = "3.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi" },
|
||||
{ name = "pytest" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e2/e8/27fcbe6516a1c956614a4b61a7fccbf3791ea0b992e07416e8948184327d/pytest_codspeed-4.2.0.tar.gz", hash = "sha256:04b5d0bc5a1851ba1504d46bf9d7dbb355222a69f2cd440d54295db721b331f7", size = 113263, upload-time = "2025-10-24T09:02:55.704Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/98/16fe3895b1b8a6d537a89eecb120b97358df8f0002c6ecd11555d6304dc8/pytest_codspeed-3.2.0.tar.gz", hash = "sha256:f9d1b1a3b2c69cdc0490a1e8b1ced44bffbd0e8e21d81a7160cfdd923f6e8155", size = 18409, upload-time = "2025-01-31T14:28:26.165Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/b8/d599a466c50af3f04001877ae8b17c12b803f3b358235736b91a0769de0d/pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609828b03972966b75b9b7416fa2570c4a0f6124f67e02d35cd3658e64312a7b", size = 261943, upload-time = "2025-10-24T09:02:37.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/19/ccc1a2fcd28357a8db08ba6b60f381832088a3850abc262c8e0b3406491a/pytest_codspeed-4.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23a0c0fbf8bb4de93a3454fd9e5efcdca164c778aaef0a9da4f233d85cb7f5b8", size = 250782, upload-time = "2025-10-24T09:02:39.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/2d/f0083a2f14ecf008d961d40439a71da0ae0d568e5f8dc2fccd3e8a2ab3e4/pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2de87bde9fbc6fd53f0fd21dcf2599c89e0b8948d49f9bad224edce51c47e26b", size = 261960, upload-time = "2025-10-24T09:02:40.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/0c/1f514c553db4ea5a69dfbe2706734129acd0eca8d5101ec16f1dd00dbc0f/pytest_codspeed-4.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95aeb2479ca383f6b18e2cc9ebcd3b03ab184980a59a232aea6f370bbf59a1e3", size = 250808, upload-time = "2025-10-24T09:02:42.07Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/04/479905bd6653bc981c0554fcce6df52d7ae1594e1eefd53e6cf31810ec7f/pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d4fefbd4ae401e2c60f6be920a0be50eef0c3e4a1f0a1c83962efd45be38b39", size = 262084, upload-time = "2025-10-24T09:02:43.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/46/d6f345d7907bac6cbb6224bd697ecbc11cf7427acc9e843c3618f19e3476/pytest_codspeed-4.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309b4227f57fcbb9df21e889ea1ae191d0d1cd8b903b698fdb9ea0461dbf1dfe", size = 251100, upload-time = "2025-10-24T09:02:44.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/dc/e864f45e994a50390ff49792256f1bdcbf42f170e3bc0470ee1a7d2403f3/pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72aab8278452a6d020798b9e4f82780966adb00f80d27a25d1274272c54630d5", size = 262057, upload-time = "2025-10-24T09:02:45.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/1c/f1d2599784486879cf6579d8d94a3e22108f0e1f130033dab8feefd29249/pytest_codspeed-4.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:684fcd9491d810ded653a8d38de4835daa2d001645f4a23942862950664273f8", size = 251013, upload-time = "2025-10-24T09:02:46.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/fd/eafd24db5652a94b4d00fe9b309b607de81add0f55f073afb68a378a24b6/pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50794dabea6ec90d4288904452051e2febace93e7edf4ca9f2bce8019dd8cd37", size = 262065, upload-time = "2025-10-24T09:02:48.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/14/8d9340d7dc0ae647991b28a396e16b3403e10def883cde90d6b663d3f7ec/pytest_codspeed-4.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0ebd87f2a99467a1cfd8e83492c4712976e43d353ee0b5f71cbb057f1393aca", size = 251057, upload-time = "2025-10-24T09:02:49.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/39/48cf6afbca55bc7c8c93c3d4ae926a1068bcce3f0241709db19b078d5418/pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbbb2d61b85bef8fc7e2193f723f9ac2db388a48259d981bbce96319043e9830", size = 267983, upload-time = "2025-10-24T09:02:50.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/86/4407341efb5dceb3e389635749ce1d670542d6ca148bd34f9d5334295faf/pytest_codspeed-4.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:748411c832147bfc85f805af78a1ab1684f52d08e14aabe22932bbe46c079a5f", size = 256732, upload-time = "2025-10-24T09:02:51.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/0e/8cb71fd3ed4ed08c07aec1245aea7bc1b661ba55fd9c392db76f1978d453/pytest_codspeed-4.2.0-py3-none-any.whl", hash = "sha256:e81bbb45c130874ef99aca97929d72682733527a49f84239ba575b5cb843bab0", size = 113726, upload-time = "2025-10-24T09:02:54.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/31/62b93ee025ca46016d01325f58997d32303752286bf929588c8796a25b13/pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5165774424c7ab8db7e7acdb539763a0e5657996effefdf0664d7fd95158d34", size = 26802, upload-time = "2025-01-31T14:28:10.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/60/2bc46bdf8c8ddb7e59cd9d480dc887d0ac6039f88c856d1ae3d29a4e648d/pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bd55f92d772592c04a55209950c50880413ae46876e66bd349ef157075ca26c", size = 25442, upload-time = "2025-01-31T14:28:11.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/56/1b65ba0ae1af7fd7ce14a66e7599833efe8bbd0fcecd3614db0017ca224a/pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf6f56067538f4892baa8d7ab5ef4e45bb59033be1ef18759a2c7fc55b32035", size = 26810, upload-time = "2025-01-31T14:28:12.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/e6/d1fafb09a1c4983372f562d9e158735229cb0b11603a61d4fad05463f977/pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a687b05c3d145642061b45ea78e47e12f13ce510104d1a2cda00eee0e36f58", size = 25442, upload-time = "2025-01-31T14:28:13.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/8b/9e95472589d17bb68960f2a09cfa8f02c4d43c82de55b73302bbe0fa4350/pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46a1afaaa1ac4c2ca5b0700d31ac46d80a27612961d031067d73c6ccbd8d3c2b", size = 27182, upload-time = "2025-01-31T14:28:15.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/18/82aaed8095e84d829f30dda3ac49fce4e69685d769aae463614a8d864cdd/pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48ce3af3dfa78413ed3d69d1924043aa1519048dbff46edccf8f35a25dab3c2", size = 25933, upload-time = "2025-01-31T14:28:17.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/15/60b18d40da66e7aa2ce4c4c66d5a17de20a2ae4a89ac09a58baa7a5bc535/pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66692506d33453df48b36a84703448cb8b22953eea51f03fbb2eb758dc2bdc4f", size = 27180, upload-time = "2025-01-31T14:28:18.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/bd/6b164d4ae07d8bea5d02ad664a9762bdb63f83c0805a3c8fe7dc6ec38407/pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:479774f80d0bdfafa16112700df4dbd31bf2a6757fac74795fd79c0a7b3c389b", size = 25923, upload-time = "2025-01-31T14:28:19.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/9b/952c70bd1fae9baa58077272e7f191f377c86d812263c21b361195e125e6/pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39", size = 15007, upload-time = "2025-01-31T14:28:24.458Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -705,6 +705,10 @@ class AzureChatOpenAI(BaseChatOpenAI):
|
||||
return self
|
||||
|
||||
def _resolve_model_profile(self) -> ModelProfile | None:
|
||||
if (self.model_name is not None) and (
|
||||
profile := _get_default_model_profile(self.model_name) or None
|
||||
):
|
||||
return profile
|
||||
if self.deployment_name is not None:
|
||||
return _get_default_model_profile(self.deployment_name) or None
|
||||
return None
|
||||
|
||||
@@ -16,6 +16,7 @@ def test_initialize_azure_openai() -> None:
|
||||
azure_deployment="35-turbo-dev",
|
||||
openai_api_version="2023-05-15",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
)
|
||||
assert llm.deployment_name == "35-turbo-dev"
|
||||
assert llm.openai_api_version == "2023-05-15"
|
||||
@@ -45,6 +46,92 @@ def test_initialize_more() -> None:
|
||||
assert ls_params.get("ls_model_name") == "gpt-35-turbo-0125"
|
||||
|
||||
|
||||
def test_profile_resolves_from_model_name() -> None:
|
||||
llm = AzureChatOpenAI(
|
||||
model="gpt-4o",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
api_version="2023-05-15",
|
||||
)
|
||||
|
||||
assert llm.profile
|
||||
assert llm.profile["name"] == "GPT-4o"
|
||||
assert llm.profile["max_input_tokens"] == 128_000
|
||||
|
||||
|
||||
def test_profile_resolves_from_model_name_with_custom_deployment_alias() -> None:
|
||||
llm = AzureChatOpenAI(
|
||||
model="gpt-4o",
|
||||
azure_deployment="35-turbo-dev",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
api_version="2023-05-15",
|
||||
)
|
||||
|
||||
assert llm.profile
|
||||
assert llm.profile["name"] == "GPT-4o"
|
||||
|
||||
|
||||
def test_profile_prefers_model_name_over_known_deployment_name() -> None:
|
||||
llm = AzureChatOpenAI(
|
||||
model="gpt-4o",
|
||||
azure_deployment="gpt-4",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
api_version="2023-05-15",
|
||||
)
|
||||
|
||||
assert llm.profile
|
||||
assert llm.profile["name"] == "GPT-4o"
|
||||
|
||||
|
||||
def test_profile_falls_back_to_deployment_name_with_unknown_model() -> None:
|
||||
llm = AzureChatOpenAI(
|
||||
model="unknown-model",
|
||||
azure_deployment="gpt-4o",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
api_version="2023-05-15",
|
||||
)
|
||||
|
||||
assert llm.profile
|
||||
|
||||
|
||||
def test_profile_resolves_from_deployment_name_without_model() -> None:
|
||||
llm = AzureChatOpenAI(
|
||||
azure_deployment="gpt-4o",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
api_version="2023-05-15",
|
||||
)
|
||||
|
||||
assert llm.profile
|
||||
assert llm.profile["name"] == "GPT-4o"
|
||||
|
||||
|
||||
def test_profile_respects_explicit_profile() -> None:
|
||||
llm = AzureChatOpenAI(
|
||||
model="gpt-4o",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
api_version="2023-05-15",
|
||||
profile={"tool_calling": False},
|
||||
)
|
||||
|
||||
assert llm.profile == {"tool_calling": False}
|
||||
|
||||
|
||||
def test_profile_is_none_for_unknown_deployment_without_model() -> None:
|
||||
llm = AzureChatOpenAI(
|
||||
azure_deployment="unknown-deployment",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
api_version="2023-05-15",
|
||||
)
|
||||
|
||||
assert llm.profile is None
|
||||
|
||||
|
||||
def test_initialize_azure_openai_with_openai_api_base_set() -> None:
|
||||
with mock.patch.dict(os.environ, {"OPENAI_API_BASE": "https://api.openai.com"}):
|
||||
llm = AzureChatOpenAI( # type: ignore[call-arg, call-arg]
|
||||
@@ -79,6 +166,7 @@ def test_structured_output_old_model() -> None:
|
||||
azure_deployment="35-turbo-dev",
|
||||
openai_api_version="2023-05-15",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
).with_structured_output(Output)
|
||||
|
||||
# assert tool calling was used instead of json_schema
|
||||
@@ -91,6 +179,7 @@ def test_max_completion_tokens_in_payload() -> None:
|
||||
azure_deployment="o1-mini",
|
||||
api_version="2024-12-01-preview",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
model_kwargs={"max_completion_tokens": 300},
|
||||
)
|
||||
messages = [HumanMessage("Hello")]
|
||||
@@ -148,6 +237,7 @@ def test_max_completion_tokens_parameter() -> None:
|
||||
azure_deployment="gpt-5",
|
||||
api_version="2024-12-01-preview",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
max_completion_tokens=1500,
|
||||
)
|
||||
messages = [HumanMessage("Hello")]
|
||||
@@ -165,6 +255,7 @@ def test_max_tokens_converted_to_max_completion_tokens() -> None:
|
||||
azure_deployment="gpt-5",
|
||||
api_version="2024-12-01-preview",
|
||||
azure_endpoint="my-base-url",
|
||||
api_key=SecretStr("test"),
|
||||
max_tokens=1000, # type: ignore[call-arg]
|
||||
)
|
||||
messages = [HumanMessage("Hello")]
|
||||
|
||||
Reference in New Issue
Block a user